Imported from Ydony/northbound-job-radar (
AGENTS.md). Install upstream withnpx skills add Ydony/northbound-job-radar. Copyright stays with the author.
Ik ben een appel: instructions for coding agents
2026-09-27 hosting direction (#193): before any hosting, runtime or database-layer
change, read docs/VPS_MIGRATION_PLAN.md and docs/HOSTING_COST_ANALYSIS.md. The owner's
direction is to develop on a VPS and later self-host at home; #193 tracks it with #194-#201
as VPS-01..VPS-08, owner Spark. No scope decision has been made and #193 carries the
gate - this is not approval to start VPS-01, and it does not change the environment
boundary below. Two findings that stop work being repeated: the Cloudflare free plan fails
on per-invocation ceilings (50 external subrequests, 10 ms CPU) rather than on volume, which
is the same wall as #192; and the port is an adapter, not a rewrite - only db/runtime.ts
imports cloudflare:workers, the D1 surface used is six methods, so the ~197 prepare()
call sites stay unchanged behind a D1Database adapter over SQLite (#195). Do not migrate
to Postgres: that turns an adapter into a dialect rewrite across those queries.
2026-09-22 dedicated Spark DEV: before verification, read WORKER-DEV.md - in your own
worktree if you are a dispatched worker, otherwise
C:/Projects/AI team and PM Tools/WORKER-DEV.md. DEV at localhost:3000 runs
this worktree with synthetic-only storage and dedicated admin/user accounts.
Use its documented existing launcher and local sign-in helper; do not restart
TEST, read owner credentials, invent another startup harness or use
/api/health for readiness. Access setup does not itself resume paused tasks.
2026-09-21 Indeed operator entry point: read docs/INDEED_TESTING.md. For an explicit
owner-requested search, use npm run indeed -- search --env test (or --env dev) after
one-time setup/login. Do not repeat phone research, scrape credentials or run acceptance
tests before routine collection. The CLI and button use the same authenticated search route,
storage and controls. Preserve local/admin scope, fixed caps and refusal/cooldown handling.
2026-09-18 Indeed work ownership: parent #63, plan docs/INDEED_INTEGRATION.md. Codex owns request/auth discovery and connection implementation (#64/#65), explicitly assigned by the owner. Other LLMs must claim one of #66-69 and respect its file boundaries. The current source remains disabled until verified. Private assessment artifacts and credentials never go into this public repo or its issues.
2026-09-19 narrow Indeed experiment exception: after the owner was explicitly asked about JobSpy's mobile identity headers, they instructed Codex to proceed. That header profile is permitted only for this local/admin experiment, behind explicit trusted configuration. TLS verification stays enabled. This is not a relaxation for other sources, proxy rotation, challenges, public access or unlimited collection.
Project board: https://github.com/users/Ydony/projects/4 — what needs doing lives here now
(migrated from Plane 2026-09-04), not in docs/TASKS.md. docs/TASKS.md stays as narrative
history of what was found/decided; don't let it re-become a second open/closed list.
Cross-agent delivery rules: read C:/Projects/AI team and PM Tools/AGENTS.md before
estimating, delegating, or reviewing work. A user scope decision is required before dispatch.
If you are a dispatched worker, read the copy in your own worktree instead - WORKER-RULES.md
and WORKER-DEV.md, written there by pm.py. Do not open either absolute path, and do not run
pm.py: you cannot reach outside your worktree, the permission request is auto-rejected because
your stdin is closed, and the run ends there with no output. Three did on 2026-09-27, on this
line. Board and project-management work is never part of a worker assignment.
Use python "C:/Projects/AI team and PM Tools/pm-tools/pm.py" (project key ajh) to
create/group epics, tasks and relations rather than writing raw GitHub/GraphQL calls.
2026-09-07 integration direction: read docs/PUBLIC_ADMIN_INTEGRATION_PLAN.md before future
source/public-service work. It records the owner's public/admin split, national-board permission
assumptions and ordered task specifications, with existing issue links. It supersedes older planning
claims about EURES reuse and an owner-only public launch; it does not mean runtime access controls,
scheduled collection or hosting already changed. Keep actual policy evidence distinct from assumptions.
Read docs/HANDOFF.md first for the state of the project and the things that will surprise you. Read
docs/MULTI_SOURCE_PLAN.md before changing discovery, job identity, filters, pipeline state, or
source adapters. Then README.md, docs/ARCHITECTURE.md, and docs/ROADMAP.md before making
product or integration changes. Local environments and the no-hosting decision:
docs/ENVIRONMENTS.md, docs/DEPLOY.md.
Environment boundary
dev(http://localhost:3000) andtest(http://localhost:3001) remain local and isolated. The owner approved a separate, private Cloudflare production environment on 2026-09-23. Its Worker, independent D1, owner-only secrets, and the sole administrator account all exist; an authenticated sign-in returned HTTP 200 on 2026-09-24, and the custom.nldomain is attached and serving over HTTPS (wwwstill needs its own redirect — see docs/DEPLOY.md). #149 tracks what's still open: the owner rotating the temporary admin password, and verifying production searches actually return results.- Dev and test must keep separate D1 state under
.wrangler/dev/stateand.wrangler/test/state. Production has its own remote D1; never copy local state or credentials into it. - Production is single-admin with closed registration. Do not open public signups,
deploy to OpenAI Sites/
chatgpt.site, or broaden hosting without a new owner decision..openai/hosting.jsonsupplies logical binding names, not a hosting target. - Keep
.dev.vars.dev,.dev.vars.test,.wrangler/, and credentials out of Git and prompts. External or older copies not under this project may still contain pre-removal CV data.
Multi-user rules
The app has accounts and roles. Every table holding user data has a user_id, and every query
must be scoped to the session user. A missing WHERE user_id = ? is a cross-account data leak.
Four such defects were found in review immediately after the tenancy change — including a workspace
reset that would have deleted every account's jobs, and a now-removed CV upload that was silently broken for
everyone. None appeared in ordinary use, because the existing data had been adopted rather than
freshly created. When you touch anything tenancy-related, exercise the whole flow as a second
account with new data, not the account that already has rows.
Uniqueness on user data is scoped per owner, never global.
Page-fetching is administrator-only and enforced server-side, not in the interface. Ordinary
accounts must never learn which page-fetched sources exist — that filtering applies to the live
search report, the stored run history, and the /sources page.
/sources and /privacy describe what the code actually does. Change them in the same commit as
any change to data handling, or they become untrue.
Product objective
Build a private job-search companion for a user seeking roles where English alone is sufficient. It provides one manually triggered, multi-source search across configured Swiss and Netherlands adapters; LinkedIn is excluded. A job is a match only when the full advertisement is predominantly English and German, French, Italian, and Dutch are not mandatory. Ambiguous ads must go to review, never pass.
Source integration boundary
- 2026-08-26, revised same day: the MVP originally forbid any jobs.ch automation
(see git history /
docs/ARCHITECTURE.mddecision record for the original reasoning). The user explicitly reversed that decision after being told: JobCloud's terms prohibit crawlers/scrapers/bots/scripting, androbots.txtseparately disallows crawling job-detail pages specifically. Automated fetching of jobs.ch (lib/job-adapters.ts,POST /api/scrape) now runs anyway, knowingly against both. Re-check current platform terms before extending this further, and don't assume the reversal generalizes to other sites without the same explicit conversation. - The line that did not move: no detection evasion. Never add randomized/human-like
timing, fingerprint spoofing, headless-browser stealth plugins, proxy/IP rotation, or
anything else designed to defeat jobs.ch's bot detection.
lib/jobsch.tsuses a plainfetch()with a standard (non-spoofed) browser User-Agent, a fixed inter-request delay, and hard caps (RESULTS_PAGE,MAX_NEW_JOBS_PER_RUN) — keep it that way. - The search is manually triggered only (the two search buttons call
POST /api/scrape) — no scheduled/cron automation exists or should be added without a fresh explicit decision, since unattended background fetching is a materially bigger step than a user-clicked action. - Do not automate jobs.ch login or application submission.
lib/jobsch.tsnever authenticates; it only reads pages that are public without a session. - A full, sanctioned jobs.ch ingestion integration (higher volume, scheduled, or authenticated) still requires written JobCloud permission or an authorized API/feed. Employer-side XML ingestion is not a public job-seeker search API.
- 2026-08-27 source expansion; portfolio retained 2026-09-09 (#32): after a source-specific terms/robots/technical review, the user explicitly asked the same manually triggered automatic behavior to cover other Swiss and Netherlands sites. The currently enabled adapters are jobs.ch, jobup.ch, JobScout24, IamExpat, and Undutchables. The three Swiss sites are JobCloud properties and therefore share the known unsanctioned-automation risk and remain administrator/VPN-only. IamExpat is administrator-only but does not require the VPN; it uses current public job paths. Undutchables remains administrator/VPN-only after prior blocking and uses only its plain public listing page, not the query-string paths disallowed by its robots policy. The private portfolio produced 12 measured English-confirmed jobs. Keep caps, fixed delays, truthful reporting and the no-evasion rule; do not expand the volume because of that result.
- The ordinary/public Indeed adapter remains
blocked: its rules prohibit automated access without written permission and direct web requests returned HTTP 403. A separate, explicitly configured Indeed experiment is available only to a loopback administrator; seedocs/INDEED_TESTING.md. Never enable it on hosted prod. Nationale Vacaturebank isunavailableafter HTTP 403, and I amsterdam isdisabledbecause it is a guide rather than a job feed. Never bypass these outcomes. LinkedIn is excluded. - Job-Room (arbeit.swiss) is live, not unavailable. An earlier note here called it
unavailable on the basis that its documented API is for employer publication. Its
unauthenticated public search and detail endpoints have been in use since 31 August
(
lib/job-room.ts, registered inlib/job-adapters.ts) and it is a full-text public source. Seedocs/SOURCE_POLICY.md§2. - Every configured source must write a truthful per-run status. Do not label a blocked, unavailable, disabled, or failed source as searched successfully.
Technical shape
- Next-compatible React app built with Vinext/Vite and the OpenAI Sites scaffold.
- Cloudflare D1 binding
DBstores account-scoped analyzed jobs and search state. - CV upload, file storage, role derivation and personal-fit scoring were removed on 2026-09-23. Historical migration versions 1–27 still mention the old schema; migration 28 removes it.
- API routes live under
app/api; deterministic language analysis lives inlib/analysis.ts. - The app is multi-user. Every user-data query must be scoped by
user_id. Search roles are explicit keywords saved insearch_roles. Language-result feedback lives separately inlanguage_feedbackso maintenance never overwrites the user's judgment.
Commands
npm install
npm run dev
npm run test:local
npm run lint
npm test
npm run typecheck
npm run build
On Windows, @rolldown/binding-win32-x64-msvc is an explicit dev dependency because npm can omit
the optional native binding. Local Miniflare/Workers state is under the environment-specific
.wrangler/dev and .wrangler/test paths and is intentionally ignored by Git.
Engineering rules
- Preserve the language result states:
pass,unknown,review,blocked. - Prefer false negatives over false positives for “English sufficient.”
- Keep the reason for every language decision visible to the user.
- Preserve the detector result when applying user language corrections; only an explicit user correction may change the effective result shown in Matches or Review.
- Never send a job description to a third-party model without explicit user consent and a documented retention policy.
- Never store VPN credentials or private tunnel keys in the project. Preserve the
provider-supported, user-visible sign-in boundary in
docs/VPN.md; do not replace it with UI automation, public proxies, proxy rotation, or IP cycling. - Validate all manually imported URLs server-side; do not trust browser input.
- Keep one SQL statement per D1
prepare()call. Add indexes for new recurring queries. - The schema lives in two hand-synced places: base creation in
db/runtime.tsand ordered applied upgrades indb/migrations.ts. Drizzle anddb/schema.tswere removed entirely, so there is no third generation model to keep in step. Add a new migration version for every schema change; never edit an applied migration or reset.wrangler/as a shortcut. Back up local state and test both existing and fresh databases. Seedocs/ARCHITECTURE.md§7a. - Do not reintroduce CV upload or personal-fit scoring without a new owner decision and data-handling review.
- Preserve user-controlled external navigation for every source login and application.
- Add tests whenever the language rules change, especially “optional” versus “mandatory” wording.
Definition of done for changes
Run lint and the stable local build, exercise the affected API or UI flow in dev and test, update the relevant documentation, and record any unresolved platform-permission or privacy issue.
Lint and build passing is not the same as working. If a flow was not actually exercised —
or was exercised and failed — say so plainly in the summary and record it in
docs/HANDOFF.md rather than implying the feature is done. Historical architecture
details are preserved in docs/ARCHITECTURE.md; use docs/FUNCTIONALITY_MAP.md for
current code paths.
