# Job·Ka ### Tous les emplois des employeurs québécois. Un seul endroit. **[www.job-ka.com](https://www.job-ka.com)** *Job·Ka collecte les offres directement sur les pages carrières des employeurs du Québec — 217 employeurs branchés sur 17 plateformes ATS, complétés par 3 portails publics (l'offre directe gagne toujours). Chaque offre est traçable à sa source, les salaires sont normalisés en $/h et $/an, et la candidature se fait chez l'employeur. Zéro boîte noire, zéro intermédiaire.*
--- ## Sommaire - [Pourquoi Job·Ka ?](#pourquoi-job-ka-) - [L'architecture en 30 secondes](#larchitecture-en-30-secondes) - [Le pipeline en détail](#le-pipeline-en-détail) - [Connecteurs](#connecteurs) - [Structure du dépôt](#structure-du-dépôt) - [Démarrage rapide](#démarrage-rapide) - [CLI `run.py`](#cli-runpy) - [Variables d'environnement](#variables-denvironnement) - [API](#api) - [Ajouter un employeur (≈ 10 lignes)](#ajouter-un-employeur--10-lignes) - [Tests](#tests) - [Production](#production) - [Principes](#principes) - [Écosystème Groupe KA](#écosystème-groupe-ka) --- ## Pourquoi Job·Ka ? Les grands sites d'emploi mélangent annonces payantes, doublons repostés et offres mortes. L'offre originale — celle de la page carrière de l'employeur — est la seule source fiable : à jour, complète, sans frais de mise en avant. > Job·Ka se branche directement sur les systèmes de recrutement (ATS) des > employeurs — 17 plateformes : Workday, Lever, Greenhouse, SmartRecruiters, > BambooHR, Breezy, Recruitee, Ashby, Workable, Taleo, Njoyn, UltiPro, > iCIMS, SuccessFactors, ADP WFN, Digital Recruiters, Workland. Un > connecteur = un employeur. Le tout resynchronisé jour et nuit. Ce n'est pas un job board qui vend de la visibilité, c'est un index fidèle de ce que les employeurs québécois affichent réellement — **avec les salaires en clair quand ils existent** (normalisés en $/h et $/an), parce que la transparence salariale est le différenciateur clé. ## L'architecture en 30 secondes ```mermaid flowchart LR subgraph Sources W["Workday"] --> C L["Lever"] --> C G["Greenhouse"] --> C S["SmartRecruiters"] --> C X["Sites maison
(HTML / Scrapfly)"] --> C end C["Connecteurs
(auto-découverts)"] --> N["finalize()
normalisation"] N --> D[("SQLite
data/jobka.db")] W2["⏱ Watcher horaire
(PM2)"] -.-> C D --> A["FastAPI
/api/*"] A --> F["React + Vite
frontend/dist"] D --> GEO["Géocodage
Nominatim + Adresses Québec"] GEO --> D ``` | Couche | Rôle | Fichiers | |---|---|---| | Connecteurs | 1 employeur = 1 module ; plateformes ATS partagées | `jobka/connectors/` | | Schéma | `JobPosting` + `finalize()` (normalisation commune) | `jobka/schema.py`, `jobka/normalize.py` | | Persistance | diff par `content_hash`, cycle de vie, anti-dérive | `jobka/db.py` | | Pipeline | ingestion, boucle `watch`, géocodage, dédup | `jobka/ingest.py`, `geocode.py`, `dedup.py` | | API + Web | FastAPI + frontend Vite servi en statique | `jobka/web.py`, `frontend/` | | Tests | fixtures HTTP enregistrées, rejeu hors ligne | `jobka/fixtures.py`, `tests/` | ## Le pipeline en détail ### Connecteurs auto-découverts Toute sous-classe de `BaseConnector` avec un `source_id` est enregistrée par `pkgutil` — aucun registre à maintenir. Les 17 plateformes ATS sont des classes de base : brancher un nouvel employeur = ~10 lignes. Politesse : throttling par source (`request_delay`), User-Agent identifiable `JobKaBot/1.0`, budgets de requêtes, cache BD des pages détail. ### Diff engine avec garde-fou anti-dérive Chaque sync compare le `content_hash` de chaque offre : ajout, mise à jour, ou disparition. Une offre absente 2 syncs consécutifs passe `active=0` (jamais de DELETE). Si une source retourne ≤ 25 % de sa médiane historique, alerte et **retraits suspendus** — un connecteur cassé ne vide pas la base. Les offres dont la `date_limite` est dépassée sont retirées même si encore affichées. ### Salaires normalisés — jamais inventés `parse_salary()` exige un signe monétaire ou une unité explicite, applique des bornes de plausibilité par unité, et convertit en $/h et $/an (40 h × 52 sem.). Une offre sans salaire reste sans salaire. L'historique des changements est conservé (`salary_log`). ### Géocodage sans invention Nominatim (1 req/1,1 s) + repli officiel Adresses Québec (MERN, lots de 200), cache persistant par lieu normalisé, validation par bounding box provinciale. Sans adresse : centroïde de ville. Introuvable : `geocode_failed=1`, jamais de coordonnées bidon. ### Déduplication inter-sources Blocage par clé exacte `titre normalisé | employeur | ville` + union-find avec exigence d'un second signal concordant (type, salaire ±4 %, date ±7 j). Le canonique est la source la plus autoritaire (page carrière avant agrégat) ; les doublons sont masqués (`dup_of`), jamais supprimés. ## Connecteurs **217 employeurs directs** connectés via **17 plateformes ATS** (Workday, Lever, Greenhouse, SmartRecruiters, BambooHR, Breezy, Recruitee, Ashby, Workable, Taleo, Njoyn, UltiPro, iCIMS, SuccessFactors, ADP WFN, Digital Recruiters, Workland) + **3 portails agrégateurs** en complément (Guichet-Emplois — Crawl-delay 5 s respecté —, Jobillico via sitemaps, Espresso-Jobs) + un **canal de dépôt direct** pour les employeurs (`/employeurs`, modération manuelle). - Les offres issues des pages carrières portent le badge **« Offre directe »** (`is_direct`) ; la candidature se fait chez l'employeur. - En cas de doublon entre une offre directe et sa copie portail, **l'offre directe gagne la canonique** (dédup direct-prioritaire, `dup_of`). - **Documentation par connecteur** (générée, standard Groupe-KA) : [`docs/connecteurs/INDEX.md`](docs/connecteurs/INDEX.md) — une fiche par plateforme ATS, par employeur, par portail et pour le dépôt direct. Régénérer : `.venv/bin/python scripts/gen_connector_docs.py`. - **Registre de conformité** : [`docs/CONFORMITE.md`](docs/CONFORMITE.md) (base d'accès par famille, attribution, zéro donnée personnelle de candidat ; Québec Emploi écarté — auth clicSÉQUR). ## Structure du dépôt ``` job-ka/ ├── run.py # point d'entrée : sync | watch | serve | geocode | record ├── requirements.txt # fastapi, uvicorn, requests, beautifulsoup4 ├── jobka/ │ ├── schema.py # dataclass JobPosting + finalize() │ ├── normalize.py # salaires, dates, mode/type, lieux, catégories │ ├── db.py # SQLite : diff, cycle de vie, anti-dérive │ ├── ingest.py # run() / watch() — orchestration │ ├── dedup.py # déduplication inter-sources (union-find) │ ├── geocode.py # Nominatim + Adresses Québec + cache │ ├── web.py # API FastAPI + /health + frontend statique │ ├── fixtures.py # enregistrement/rejeu HTTP pour les tests │ └── connectors/ │ ├── base.py # BaseConnector (throttling, Scrapfly, cache détail) │ ├── workday.py # plateforme Workday (facette Québec auto-découverte) │ ├── .py # 16 autres plateformes ATS partagées │ ├── guichet_emplois.py # + jobillico.py, espresso_jobs.py : portails │ └── .py # 1 fichier = 1 employeur (sous-classe ~10 lignes) ├── data/sources.json # registre descriptif des sources (221 entrées) ├── frontend/ # React 18 + Vite + TS (carte MapLibre, sans jeton) ├── tests/ # pytest : normalisation + rejeu des connecteurs ├── scripts/check_headers.py # hook pre-commit : en-tête d'auteur obligatoire ├── scripts/gen_connector_docs.py # génère docs/connecteurs/ (fiches + INDEX) └── docs/ # connecteurs/, CONFORMITE.md, déploiement ``` ## Démarrage rapide ```bash git clone job-ka && cd job-ka # Backend python3 -m venv .venv .venv/bin/pip install -r requirements.txt # Frontend cd frontend && npm install && npm run build && cd .. # Secrets (Scrapfly pour les sites anti-bot — facultatif au départ) cp .env.example .env # puis remplir SCRAPFLY_API_KEY # Amorçage : première synchronisation + géocodage .venv/bin/python run.py sync .venv/bin/python run.py geocode # Service (API + frontend) — http://localhost:8096 .venv/bin/python run.py serve 8096 ``` En dev frontend : `npm run dev` dans `frontend/` (proxy `/api` → :8096). ## CLI `run.py` | Commande | Rôle | |---|---| | `run.py sync [source ...]` | synchronise les offres (toutes les sources ou celles données) | | `run.py watch [minutes]` | boucle : sync + géocodage + dédup (défaut 60 min) | | `run.py serve [port]` | API + frontend (défaut 8096) | | `run.py geocode [n]` | géocode les offres sans coordonnées | | `run.py record ` | enregistre les fixtures de test d'une source | ## Variables d'environnement | Variable | Rôle | |---|---| | `SCRAPFLY_API_KEY` | rendu JS / anti-bot pour les pages carrières maison | | `JOBKA_WORKDAY_DETAIL_LIMIT` | plafond de pages détail Workday par sync (défaut 300) | | `JOBKA_SR_DETAIL_LIMIT` | plafond de pages détail SmartRecruiters (défaut 300) | | `JOBKA__*` | budgets par plateforme/portail (pages, détails, fenêtres) — défauts et rôle documentés dans [`docs/connecteurs/`](docs/connecteurs/INDEX.md) | ## API | Route | Rôle | |---|---| | `GET /health` | healthcheck (BD + fraîcheur des syncs) | | `GET /api/jobs` | recherche : `q, city, category, work_mode, employment_type, salary_min, with_salary, sort, limit, offset` | | `GET /api/jobs.geojson` | offres géolocalisées (carte), `bbox` + mêmes filtres | | `GET /api/jobs/{uid}` | fiche complète + historique salarial | | `GET /api/facets` | valeurs distinctes pour les filtres | | `GET /api/sources` | registre des employeurs + fraîcheur par source | | `GET /api/stats` | totaux, catégories, villes, dernières syncs | | `POST /api/sync` | déclenche une synchronisation (webhook) | | `POST /api/employeurs/offres` | dépôt direct d'une offre par un employeur (modération manuelle) | ## Ajouter un employeur (≈ 10 lignes) ```python # jobka/connectors/mon_employeur.py (+ en-tête d'auteur, règle nº 1) from .workday import WorkdayConnector class MonEmployeurConnector(WorkdayConnector): source_id = "mon_employeur" EMPLOYER = "Mon Employeur inc." TENANT = "monemployeur" # monemployeur.wd3.myworkdayjobs.com HOST = "wd3" SITE = "Careers" ``` Puis : ajouter l'entrée dans `data/sources.json`, `run.py sync mon_employeur` pour valider, `run.py record mon_employeur` pour figer les fixtures de test. ## Tests ```bash .venv/bin/python -m pytest # tout .venv/bin/python -m pytest tests/test_normalize.py # normalisation seule ``` - `test_normalize.py` — salaires, dates, modes, types, lieux, catégories ; - `test_connectors.py` — rejeu hors ligne des fixtures : instantané exact + invariants (uid uniques, URLs absolues, salaires plausibles, valeurs jamais inventées) + `finalize()` sûr. ## Production ``` job-ka-web .venv/bin/python run.py serve 8096 # API + frontend job-ka-sync .venv/bin/python run.py watch 60 # resync horaire, jour et nuit job-ka-ngrok ngrok http --url=www.job-ka.com 8096 # tunnel public ``` **Philosophie d'exploitation : on ne pousse que le code — le serveur maintient ses données lui-même.** `data/*.db`, `frontend/dist/` et `.env` ne sont jamais versionnés. Détails : `docs/deploiement.md`. ## Principes 1. **Politesse** — throttling par source, robots.txt respecté, User-Agent identifiable, budgets de requêtes, caches. 2. **Fidélité** — les champs reflètent la source ; une donnée absente reste absente, jamais de valeur inventée (salaire, date, coordonnées). 3. **Traçabilité** — chaque offre garde son URL originale ; la candidature se fait chez l'employeur. Zéro boîte noire. 4. **Robustesse** — une source cassée ne bloque pas les autres et ne vide pas la base (délai de grâce + anti-dérive). 5. **Transparence salariale** — salaires numériques normalisés en $/h et $/an ; filtre « salaire affiché » de premier ordre. ## Écosystème Groupe KA Job·Ka partage la philosophie et l'architecture des plateformes sœurs : [Lou·Ka](https://www.lou-ka.com) (logements), Immo·Ka, Vrai-Prix, ValoPlex, Auto·Ka, Fabri·Ka, Food·Ka. ---
## Auteur **Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai) © 2026 Groupe KA — Simon-Pierre Boucher. Tous droits réservés.