SPB Git

spb/job-ka

Public

[ka6] fix connecteur h2o_innovation: le babillard fr_CA du tenant ADP WFN a cessé d'exposer requisitionLocations (liste+détail, 0/11) entre 00:06 et 02:35 EDT le 2026-10-23 — même nuit et même mécanique que bento_sushi (e0743907) — le filtre Québec rejetait les 10 offres pourtant toujours publiées (mêmes itemID). Particularité H2O vs bento: le babillard en_US du MÊME tenant expose encore ses locations (19/19) mais ne partage que ~3 itemID avec fr_CA, et les descriptions ne publient pas d'adresse complète (seulement « usine située à Ham-Nord », « bureau de Québec »). Correctif scoped au connecteur (_keep + _resolve_blind): une réquisition sans location passe le filtre puis son lieu est résolu sans rien inventer — 1) offre active connue en base = ville/code postal qu'ADP exposait avant la panne (offre revalidée live par la liste), 2) jointure par itemID sur le babillard en_US (locations ADP live; hors-QC écartée — cas réel « L'ingénieur.e de procédés » Burlington ON), 3) ville citée dans la description de l'employeur validée par region_for_city. Une offre sans marqueur QC est écartée; chemin normal repris tel quel si ADP ré-expose les locations fr_CA (fallback dormant). Sync isolé rejoué: ok=1 found=10 (= médiane 10.0) en 20,8 s, 10 actives avec ville/postal/région/GPS 100 %. 108 tests verts, pm2 restart job-ka-sync OK, site local 200, www.job-ka.com 200. Fiches h2o_innovation.md/ats-adp.md/INDEX.md régénérées (1131 autres fiches restaurées, hors périmètre).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 23, 2026) parent e074390 Browse files

4 changed files +138 −20

modified docs/connecteurs/INDEX.md +2 −2
@@ -5,14 +5,14 @@ Auteur : Simon-Pierre Boucher
5 Contact : contact@spboucher.ai 5 Contact : contact@spboucher.ai
6 Fichier : docs/connecteurs/INDEX.md 6 Fichier : docs/connecteurs/INDEX.md
7 Rôle : Index généré de la documentation des connecteurs (ATS, employeurs, portails, dépôt) 7 Rôle : Index généré de la documentation des connecteurs (ATS, employeurs, portails, dépôt)
8−Généré : 2026-10-23 05:25 EDT par scripts/gen_connector_docs.py — NE PAS ÉDITER, 8+Généré : 2026-10-23 05:43 EDT par scripts/gen_connector_docs.py — NE PAS ÉDITER,
9 relancer le script pour régénérer. 9 relancer le script pour régénérer.
10 ============================================================================= 10 =============================================================================
11 --> 11 -->
12 12
13 # Job·Ka — Connecteurs (index) 13 # Job·Ka — Connecteurs (index)
14 14
15−> Index généré le 2026-10-23 05:25 EDT par `scripts/gen_connector_docs.py` (rejouable). Croise `data/sources.json`, l'introspection de `jobka/connectors/*.py` et la base live `data/jobka.db`. 15+> Index généré le 2026-10-23 05:43 EDT par `scripts/gen_connector_docs.py` (rejouable). Croise `data/sources.json`, l'introspection de `jobka/connectors/*.py` et la base live `data/jobka.db`.
16 16
17 **1104 employeurs directs** connectés via **25 plateformes ATS** + **3 portails agrégateurs** + le canal de **dépôt direct** — **25937 offres actives** (23091 directes, 2846 portails ; 162 masquées comme doublons inter-sources, soit 25775 visibles). 1134 fiches. 17 **1104 employeurs directs** connectés via **25 plateformes ATS** + **3 portails agrégateurs** + le canal de **dépôt direct** — **25937 offres actives** (23091 directes, 2846 portails ; 162 masquées comme doublons inter-sources, soit 25775 visibles). 1134 fiches.
18 18
modified docs/connecteurs/ats-adp.md +5 −5
@@ -5,7 +5,7 @@ Auteur : Simon-Pierre Boucher
5 Contact : contact@spboucher.ai 5 Contact : contact@spboucher.ai
6 Fichier : docs/connecteurs/ats-adp.md 6 Fichier : docs/connecteurs/ats-adp.md
7 Rôle : Fiche plateforme ATS adp — mécanique partagée + tableau des 17 tenants 7 Rôle : Fiche plateforme ATS adp — mécanique partagée + tableau des 17 tenants
8−Généré : 2026-10-23 05:25 EDT par scripts/gen_connector_docs.py — NE PAS ÉDITER, 8+Généré : 2026-10-23 05:43 EDT par scripts/gen_connector_docs.py — NE PAS ÉDITER,
9 relancer le script pour régénérer. 9 relancer le script pour régénérer.
10 ============================================================================= 10 =============================================================================
11 --> 11 -->
@@ -46,12 +46,12 @@ Généré : 2026-10-23 05:25 EDT par scripts/gen_connector_docs.py — NE PAS
46 | [Strathmore](strathmore.md) | 11 | 45 % | 100 % | 9 % | 2026-10-23 03:15 | ✅ | 46 | [Strathmore](strathmore.md) | 11 | 45 % | 100 % | 9 % | 2026-10-23 03:15 | ✅ |
47 | [Quincaillerie Richelieu](richelieu.md) | 11 | 0 % | 100 % | 0 % | 2026-10-23 03:04 | ✅ | 47 | [Quincaillerie Richelieu](richelieu.md) | 11 | 0 % | 100 % | 0 % | 2026-10-23 03:04 | ✅ |
48 | [Cook It](cookit.md) | 11 | 64 % | 100 % | 0 % | 2026-10-23 04:26 | ✅ | 48 | [Cook It](cookit.md) | 11 | 64 % | 100 % | 0 % | 2026-10-23 04:26 | ✅ |
49−| [Canadian Royalties](canadian_royalties.md) | 10 | 40 % | 100 % | 0 % | 2026-10-23 04:03 | ✅ | 49+| [Canadian Royalties](canadian_royalties.md) | 10 | 40 % | 100 % | 0 % | 2026-10-23 05:35 | ✅ |
50−| [H2O Innovation](h2o_innovation.md) | 10 | 10 % | 100 % | 0 % | 2026-10-23 04:58 | ✅ | 50+| [H2O Innovation](h2o_innovation.md) | 10 | 10 % | 100 % | 0 % | 2026-10-23 05:43 | ✅ |
51−| [Bento Sushi](bento_sushi.md) | 8 | 88 % | 100 % | 0 % | 2026-10-23 05:25 | ✅ | 51+| [Bento Sushi](bento_sushi.md) | 8 | 88 % | 100 % | 0 % | 2026-10-23 05:33 | ✅ |
52 | [MSI Gestion immobilière](msi_gestion.md) | 6 | 100 % | 100 % | 50 % | 2026-10-23 02:55 | ✅ | 52 | [MSI Gestion immobilière](msi_gestion.md) | 6 | 100 % | 100 % | 50 % | 2026-10-23 02:55 | ✅ |
53 | [Cultures Gen V](cultures_genv.md) | 5 | 20 % | 100 % | 0 % | 2026-10-23 04:26 | ✅ | 53 | [Cultures Gen V](cultures_genv.md) | 5 | 20 % | 100 % | 0 % | 2026-10-23 04:26 | ✅ |
54−| [Abritek](abritek.md) | 5 | 100 % | 100 % | 0 % | 2026-10-23 03:53 | ✅ | 54+| [Abritek](abritek.md) | 5 | 100 % | 100 % | 0 % | 2026-10-23 05:26 | ✅ |
55 | [Mitsubishi HC Capital Canada](mitsubishi_hc_capital.md) | 1 | 0 % | 100 % | 0 % | 2026-10-23 02:54 | ✅ | 55 | [Mitsubishi HC Capital Canada](mitsubishi_hc_capital.md) | 1 | 0 % | 100 % | 0 % | 2026-10-23 02:54 | ✅ |
56 | [Osgoode Properties](osgoode_properties.md) | 1 | 100 % | 100 % | 0 % | 2026-10-23 02:57 | ✅ | 56 | [Osgoode Properties](osgoode_properties.md) | 1 | 100 % | 100 % | 0 % | 2026-10-23 02:57 | ✅ |
57 | [Niobec (Magris Performance Materials)](niobec.md) | 1 | 100 % | 100 % | 0 % | 2026-10-23 02:56 | ✅ | 57 | [Niobec (Magris Performance Materials)](niobec.md) | 1 | 100 % | 100 % | 0 % | 2026-10-23 02:56 | ✅ |
modified docs/connecteurs/h2o_innovation.md +7 −7
@@ -5,7 +5,7 @@ Auteur : Simon-Pierre Boucher
5 Contact : contact@spboucher.ai 5 Contact : contact@spboucher.ai
6 Fichier : docs/connecteurs/h2o_innovation.md 6 Fichier : docs/connecteurs/h2o_innovation.md
7 Rôle : Fiche connecteur — H2O Innovation (employeur direct, ATS adp) 7 Rôle : Fiche connecteur — H2O Innovation (employeur direct, ATS adp)
8−Généré : 2026-10-14 18:40 EDT par scripts/gen_connector_docs.py — NE PAS ÉDITER, 8+Généré : 2026-10-23 05:43 EDT par scripts/gen_connector_docs.py — NE PAS ÉDITER,
9 relancer le script pour régénérer. 9 relancer le script pour régénérer.
10 ============================================================================= 10 =============================================================================
11 --> 11 -->
@@ -38,7 +38,7 @@ Généré : 2026-10-14 18:40 EDT par scripts/gen_connector_docs.py — NE PAS
38 38
39 ## Champs → schéma 39 ## Champs → schéma
40 40
41−Complétude mesurée sur les **10 offres actives** de la source (base live, 2026-10-14 18:40 EDT). 41+Complétude mesurée sur les **10 offres actives** de la source (base live, 2026-10-23 05:43 EDT).
42 42
43 | Champ (`jobs.*`) | Rôle | Complétude | 43 | Champ (`jobs.*`) | Rôle | Complétude |
44 |---|---|---| 44 |---|---|---|
@@ -64,7 +64,7 @@ Complétude mesurée sur les **10 offres actives** de la source (base live, 2026
64 | `apply_url` — Candidature | lien de candidature directe | 0 % | 64 | `apply_url` — Candidature | lien de candidature directe | 0 % |
65 | `gps` — GPS | lat/lng (géocodage sans invention) | 100 % | 65 | `gps` — GPS | lat/lng (géocodage sans invention) | 100 % |
66 66
67−Exemple réel : *Technicien.ne de service* — Ham-Nord · ∅ · parue le 2026-10-07. 67+Exemple réel : *Programmeur.e automatisation et contrôles* — Ham-Nord · ∅ · parue le 2026-10-23.
68 68
69 ## Fréquence & budgets 69 ## Fréquence & budgets
70 70
@@ -79,15 +79,15 @@ Exemple réel : *Technicien.ne de service* — Ham-Nord · ∅ · parue le 2026-
79 79
80 ## Volumétrie & complétude live 80 ## Volumétrie & complétude live
81 81
82−*(mesuré le 2026-10-14 18:40 EDT dans `data/jobka.db`)* 82+*(mesuré le 2026-10-23 05:43 EDT dans `data/jobka.db`)*
83 83
84−- **Offres en base** : 17 (dont **10 actives**, 0 masquées comme doublons `dup_of`) 84+- **Offres en base** : 19 (dont **10 actives**, 0 masquées comme doublons `dup_of`)
85 - **Salaire** : 1/10 actives (10 %) 85 - **Salaire** : 1/10 actives (10 %)
86 - **GPS** : 10/10 actives (100 %) 86 - **GPS** : 10/10 actives (100 %)
87 - **Mode** : 0/10 actives (0 %) 87 - **Mode** : 0/10 actives (0 %)
88 - **Parution** : 10/10 actives (100 %) 88 - **Parution** : 10/10 actives (100 %)
89−- **Dernier sync** : 2026-10-14 15:17 — trouvées 10, +0, ~0, -0 89+- **Dernier sync** : 2026-10-23 05:43 — trouvées 10, +0, ~10, -0
90−- **Syncs enregistrés** (`sync_log`) : 485 90+- **Syncs enregistrés** (`sync_log`) : 562
91 91
92 ## Erreurs & dépannage 92 ## Erreurs & dépannage
93 93
modified jobka/connectors/h2o_innovation.py +124 −6
@@ -5,8 +5,8 @@
5 # Fichier : jobka/connectors/h2o_innovation.py 5 # Fichier : jobka/connectors/h2o_innovation.py
6 # Rôle : Connecteur H2O Innovation — traitement de l'eau, Québec — ADP WFN 6 # Rôle : Connecteur H2O Innovation — traitement de l'eau, Québec — ADP WFN
7 # + repli « détail seulement » quand l'API liste échoue 7 # + repli « détail seulement » quand l'API liste échoue
8−# (maintenances nocturnes récurrentes d'ADP) 8+# + résolution de lieu quand ADP n'expose plus requisitionLocations
9−# Créé : 2026-08-25 Modifié : 2026-08-30 9+# Créé : 2026-08-25 Modifié : 2026-10-23
10 # ============================================================================= 10 # =============================================================================
11 """H2O Innovation — ADP Workforce Now, avec repli de revalidation. 11 """H2O Innovation — ADP Workforce Now, avec repli de revalidation.
12 12
@@ -18,7 +18,7 @@ column 1 » du sync de 22:19 (29/08). Comme le cycle complet met ~3 h entre
18 deux passages de la source, un seul passage raté pendant la fenêtre suffit à 18 deux passages de la source, un seul passage raté pendant la fenêtre suffit à
19 dépasser le seuil stale de 4 h. 19 dépasser le seuil stale de 4 h.
20 20
21−D'où le repli ci-dessous (même pattern que cultures_genv.py / 21+D'où le repli « détail seulement » (même pattern que cultures_genv.py /
22 groupe_nordik.py / ciena.py) : si la pagination liste échoue en 5xx ou en 22 groupe_nordik.py / ciena.py) : si la pagination liste échoue en 5xx ou en
23 non-JSON, on revalide une à une les offres actives connues en base via l'API 23 non-JSON, on revalide une à une les offres actives connues en base via l'API
24 détail job-requisitions/<itemID> (qui renvoie la réquisition complète : 24 détail job-requisitions/<itemID> (qui renvoie la réquisition complète :
@@ -29,13 +29,45 @@ sorry.adp.com), l'erreur d'origine est propagée pour que le sync reste un
29 échec (inventaire préservé). Limite assumée : le repli ne découvre pas les 29 échec (inventaire préservé). Limite assumée : le repli ne découvre pas les
30 NOUVELLES offres — le chemin liste normal reprend seul au cycle suivant la 30 NOUVELLES offres — le chemin liste normal reprend seul au cycle suivant la
31 fin de la maintenance. 31 fin de la maintenance.
32+
33+Chronologie (mission ka6 2026-10-23) : entre le sync vert de 00:06 EDT
34+(found=10) et celui de 02:35 (found=0), le babillard fr_CA du tenant a CESSÉ
35+d'exposer `requisitionLocations` (liste ET détail, 0/11 — même nuit et même
36+mécanique que bento_sushi e0743907), alors que les 10 offres QC restent
37+publiées (mêmes itemID). Particularité H2O : le babillard en_US du MÊME
38+tenant expose encore ses locations (19/19), mais ne partage que ~3 itemID
39+avec le babillard fr_CA ; et les descriptions ne publient pas d'adresse
40+complète, seulement des mentions comme « usine située à Ham-Nord » ou
41+« bureau de Québec ».
42+
43+Adaptation (`_keep` + `_resolve_blind`) : une réquisition sans location
44+passe le filtre plateforme, puis son lieu est résolu sans rien inventer,
45+dans l'ordre :
46+1. offre ACTIVE CONNUE en base → ville/code postal qu'ADP exposait avant la
47+ panne (l'offre est revalidée live : même itemID toujours publié) ;
48+2. jointure par itemID sur le babillard en_US du tenant (locations ADP
49+ live) — une offre localisée hors Québec y est ÉCARTÉE (cas réel :
50+ « L'ingénieur.e de procédés », Burlington ON) ;
51+3. ville citée dans la description publiée par l'employeur, validée contre
52+ les municipalités du Québec (region_for_city).
53+Une offre sans aucun marqueur québécois est écartée. Si ADP ré-expose les
54+locations fr_CA, le chemin normal reprend tel quel (fallback dormant).
32 """ 55 """
33 from __future__ import annotations 56 from __future__ import annotations
34 57
58+import re
59+
35 import requests 60 import requests
36 61
62+from ..regions import region_for_city
37 from ..schema import JobPosting, clean_html 63 from ..schema import JobPosting, clean_html
38−from .adp import _API, ADPWorkforceNowConnector 64+from .adp import PAGE_SIZE, _API, ADPWorkforceNowConnector
65+
66+# « …usine située à Ham-Nord », « …bureau de Québec », « …équipe à Ham-Nord »
67+_RX_DESC_CITY = re.compile(
68+ r"(?:situ[ée]{1,2}s?\s+à|bas[ée]{1,2}s?\s+à|équipe\s+à"
69+ r"|(?:bureau|usine|succursale|bureaux)\s+(?:de\s+|d['’]))\s*"
70+ r"([A-ZÀ-ÿ][A-Za-zà-ÿ'’\-]+(?:[ -][A-ZÀ-ÿ][A-Za-zà-ÿ'’\-]+)*)")
39 71
40 72
41 class H2oInnovationConnector(ADPWorkforceNowConnector): 73 class H2oInnovationConnector(ADPWorkforceNowConnector):
@@ -46,7 +78,7 @@ class H2oInnovationConnector(ADPWorkforceNowConnector):
46 78
47 def fetch(self) -> list[JobPosting]: 79 def fetch(self) -> list[JobPosting]:
48 try: 80 try:
49− return super().fetch() 81+ jobs = super().fetch()
50 except (requests.RequestException, ValueError) as exc: 82 except (requests.RequestException, ValueError) as exc:
51 resp = getattr(exc, "response", None) 83 resp = getattr(exc, "response", None)
52 if resp is not None and resp.status_code < 500: 84 if resp is not None and resp.status_code < 500:
@@ -54,7 +86,93 @@ class H2oInnovationConnector(ADPWorkforceNowConnector):
54 jobs = self._fetch_known_jobs() 86 jobs = self._fetch_known_jobs()
55 if jobs is None: 87 if jobs is None:
56 raise # API détail cassée aussi : vraie panne totale 88 raise # API détail cassée aussi : vraie panne totale
57− return jobs 89+ return self._resolve_blind(jobs)
90+
91+ def _keep(self, locations: list[dict]) -> bool:
92+ # Sans location (panne requisitionLocations du 2026-10-23), impossible
93+ # de trancher ici : on laisse passer et _resolve_blind tranchera.
94+ if not locations:
95+ return True
96+ return super()._keep(locations)
97+
98+ def _known_locations(self) -> dict[str, tuple[str, str, str]]:
99+ """itemID → (ville, code postal, libellé) des offres actives en base
100+ — le lieu qu'ADP exposait lui-même avant la panne."""
101+ from .. import db
102+ con = db.connect()
103+ try:
104+ rows = con.execute(
105+ "SELECT external_id, city, postal_code, location_label "
106+ "FROM jobs WHERE source=? AND active=1 AND city != ''",
107+ (self.source_id,)).fetchall()
108+ finally:
109+ con.close()
110+ return {str(eid): (city, postal or "", label or "")
111+ for eid, city, postal, label in rows if eid}
112+
113+ def _en_board_locations(self) -> dict[str, list[dict]]:
114+ """itemID → locations du babillard en_US du tenant (locations ADP
115+ live, encore exposées là alors que fr_CA est aveugle)."""
116+ out: dict[str, list[dict]] = {}
117+ skip = 0
118+ for _ in range(self.max_pages):
119+ try:
120+ data = self._get_json(_API, params=self._params(
121+ {"lang": "en_US", "locale": "en_US",
122+ "$top": str(PAGE_SIZE), "$skip": str(skip)}))
123+ except (requests.RequestException, ValueError, RuntimeError):
124+ return out # babillard en_US indisponible : signal absent
125+ reqs = data.get("jobRequisitions") or []
126+ if not reqs:
127+ break
128+ for r in reqs:
129+ iid = str(r.get("itemID") or "")
130+ if iid:
131+ out[iid] = self._locations(r)
132+ skip += len(reqs) # l'API plafonne parfois sous $top
133+ return out
134+
135+ def _city_from_description(self, text: str) -> str:
136+ """Ville QC citée dans la description de l'employeur, sinon ''."""
137+ for m in _RX_DESC_CITY.finditer(text or ""):
138+ city = m.group(1).strip()
139+ if region_for_city(city):
140+ return city
141+ return ""
142+
143+ def _resolve_blind(self, jobs: list[JobPosting]) -> list[JobPosting]:
144+ """Résout le lieu des offres sans location (rien n'est inventé)."""
145+ if all(j.city for j in jobs):
146+ return jobs # locations ADP normales : rien à faire
147+ known = self._known_locations()
148+ en_locs: dict[str, list[dict]] | None = None # paresseux (2e appel API)
149+ kept: list[JobPosting] = []
150+ for job in jobs:
151+ if job.city:
152+ kept.append(job)
153+ continue
154+ row = known.get(job.external_id)
155+ if row: # offre connue, revalidée live par la liste
156+ job.city, job.postal_code, job.location_label = row
157+ kept.append(job)
158+ continue
159+ if en_locs is None:
160+ en_locs = self._en_board_locations()
161+ locs = en_locs.get(job.external_id)
162+ if locs:
163+ qc = next((l for l in locs if l["prov"].upper() == "QC"), None)
164+ if qc is None:
165+ continue # localisée hors Québec : écartée
166+ job.city, job.postal_code = qc["city"], qc["postal"]
167+ job.location_label = qc["label"]
168+ kept.append(job)
169+ continue
170+ city = self._city_from_description(job.description)
171+ if not city:
172+ continue # aucun marqueur québécois : écartée
173+ job.city = city
174+ kept.append(job)
175+ return kept
58 176
59 def _known_ids(self) -> list[str]: 177 def _known_ids(self) -> list[str]:
60 """itemID (external_id) des offres actives de la source en base.""" 178 """itemID (external_id) des offres actives de la source en base."""
61 179
Menu

Theme