SPB Git

Repository Public

Commit

docs: README à jour avec screenshot

Simon-Pierre Boucher committed · Aug 18, 2026 parent 70ce331
2 changed files +106 −350 Browse files
modified README.md +106 −350
@@ -1,10 +1,10 @@
1−<div align="center">
2−
3 # Lou·Ka 1 # Lou·Ka
4 2
5−### Tous les logements à louer du Québec. Un seul endroit. 3+### Tous les **logements à louer du Québec**. Un seul endroit.
4+
5+**[www.lou-ka.com](https://www.lou-ka.com)** — un service **[Groupe Ka](https://www.groupe-ka.com)**
6 6
7−**[www.lou-ka.com](https://www.lou-ka.com)** 7+![Aperçu de Lou-Ka](docs/screenshot.png)
8 8
9 ![Python](https://img.shields.io/badge/Python-3.14-141814?style=for-the-badge&logo=python&logoColor=d9f26b) 9 ![Python](https://img.shields.io/badge/Python-3.14-141814?style=for-the-badge&logo=python&logoColor=d9f26b)
10 ![FastAPI](https://img.shields.io/badge/FastAPI-API_+_SSR-141814?style=for-the-badge&logo=fastapi&logoColor=d9f26b) 10 ![FastAPI](https://img.shields.io/badge/FastAPI-API_+_SSR-141814?style=for-the-badge&logo=fastapi&logoColor=d9f26b)
@@ -20,260 +20,94 @@
20 ![Régions](https://img.shields.io/badge/r%C3%A9gions-11-1c5c41?style=flat-square) 20 ![Régions](https://img.shields.io/badge/r%C3%A9gions-11-1c5c41?style=flat-square)
21 ![Géocodées](https://img.shields.io/badge/g%C3%A9olocalis%C3%A9es-70%25-1c5c41?style=flat-square) 21 ![Géocodées](https://img.shields.io/badge/g%C3%A9olocalis%C3%A9es-70%25-1c5c41?style=flat-square)
22 22
23−*Agrégateur indépendant de logements locatifs — chaque annonce avec toutes ses photos,
24−ses détails standardisés, son quartier documenté, et un lien direct vers l'annonce
25−originale du gestionnaire. Toujours à jour, automatiquement.*
26−
27−<img src="docs/screenshots/accueil.png" alt="Page d'accueil Lou-Ka" width="920">
28−
29−</div>
30−
31 --- 23 ---
32 24
33−## Sommaire 25+## Description
34−
35−- [Pourquoi Lou-Ka ?](#pourquoi-lou-ka-)
36−- [Visite guidée](#visite-guidée)
37−- [L'architecture en 30 secondes](#larchitecture-en-30-secondes)
38−- [Le pipeline en détail](#le-pipeline-en-détail)
39−- [Lou-Ka Maps](#lou-ka-maps)
40−- [SEO — SSR léger, sitemaps, JSON-LD](#seo--ssr-léger-sitemaps-json-ld)
41−- [Comptes, SSO KA & profils](#comptes-sso-ka--profils)
42−- [Stats de marché & PDF](#stats-de-marché--pdf)
43−- [App iOS native](#app-ios-native)
44−- [Structure du dépôt](#structure-du-dépôt)
45−- [Démarrage rapide](#démarrage-rapide)
46−- [CLI `run.py`](#cli-runpy)
47−- [Variables d'environnement](#variables-denvironnement)
48−- [API](#api)
49−- [Ajouter un gestionnaire (≈ 30 lignes)](#ajouter-un-gestionnaire--30-lignes)
50−- [Tests](#tests)
51−- [Couverture](#couverture)
52−- [Production](#production)
53−- [Principes](#principes)
54 26
55−--- 27+Chercher un appartement au Québec, c'est ouvrir des dizaines de sites web différents — chacun avec sa navigation, ses filtres, son format. **Lou-Ka retourne le problème** : un **connecteur dédié par gestionnaire immobilier** visite chaque site, **normalise chaque annonce** vers un schéma unique, et **détecte les changements en continu**.
56 28
57−## Pourquoi Lou-Ka ? 29+Lou-Ka n'est pas une plateforme d'annonces : c'est un **index fidèle** et un **agrégateur indépendant** de logements locatifs. **Aucun prix inventé**, **aucune coordonnée devinée**, et chaque fiche renvoie vers **l'annonce originale du gestionnaire** via une passerelle de sortie transparente (`/passerelle/{uid}`).
58 30
59−Chercher un appartement au Québec, c'est ouvrir des dizaines de sites web différents — 31+> Les sites d'agences n'offrent pas de webhooks. Lou-Ka reproduit l'équivalent : **synchronisation périodique + hash de contenu** → ajouts, mises à jour et retraits détectés automatiquement. Une annonce qui disparaît du site source disparaît de Lou-Ka (et répond **`410 Gone`** aux moteurs de recherche).
60−chacun avec sa propre navigation, ses propres filtres, son propre format. **Lou-Ka
61−retourne le problème** : un connecteur dédié par gestionnaire immobilier visite chaque
62−site, normalise chaque annonce vers un schéma unique, et détecte les changements en
63−continu.
64 32
65−> Les sites d'agences n'offrent pas de webhooks. Lou-Ka reproduit l'équivalent : 33+**En chiffres** : **22 900+ annonces actives**, **265 sources recensées**, **205 connecteurs**, **820 villes**, **11 régions**, **~70 %** des annonces géolocalisées.
66−> **synchronisation périodique + hash de contenu** → ajouts, mises à jour et retraits
67−> détectés automatiquement. Une annonce qui disparaît du site source disparaît de Lou-Ka
68−> (et répond `410 Gone` aux moteurs de recherche).
69 34
70−Lou-Ka n'est pas une plateforme d'annonces : c'est un **index fidèle**. Aucun prix 35+## Fonctionnalités
71−inventé, aucune coordonnée devinée, et chaque fiche renvoie vers l'annonce originale
72−du gestionnaire via une passerelle de sortie transparente.
73 36
74−## Visite guidée 37+- **Recherche filtrée** — ville, quartier, type (**3½**, 4½…), loyer min/max, animaux, meublé, superficie, texte libre.
38+- **Vue carte 3D** — **Lou-Ka Maps** (framework **`@groupe-ka/ka-maps`**, moteur **Mapbox GL JS**) : clusters par prix moyen, « Rechercher dans cette zone », caméra partageable dans l'URL, bascule **3D/2D**, synchro liste ↔ carte.
39+- **Fiches complètes** — **toutes les photos**, digest structuré de la description (**text mining regex FR, aucun LLM**), badge marché, **historique de prix** (`price_log`), bloc « **Le quartier** » (recensement 2021, INSPQ, SPVM, écoles MEQ, POI OSM), **fiche PDF** avec QR code.
40+- **Pages villes SEO** — `/villes`, `/ville/{slug}`, `/ville/{slug}/{type}` avec **SSR léger**, sitemaps dynamiques (chunks de **10 000**), **JSON-LD** (`RealEstateListing` + `Offer` CAD), vrais `404`/`410`.
41+- **Observatoire du marché** — KPI temps réel, loyers médians par région/ville/taille, **rapport de marché PDF** multi-pages (`/api/stats/rapport.pdf`).
42+- **Registre des sources** — chaque gestionnaire, son statut, sa dernière synchro ; toute source **non connectable est documentée avec sa raison** dans `data/sources.json`.
43+- **Comptes & SSO KA** — « **Se connecter avec KA** » via le hub **groupe-ka.com** (JWT HS256, audience `lou-ka`, **`ka_id`** émis par le hub) + **Google OAuth** direct ; favoris synchronisés vers « Mon univers Ka » ; pages gestionnaires réclamables (`/g/{source_id}`) ; profil public **opt-in** `/u/{ka_id}`.
44+- **Widget KA Agent** — bulle de chat IA du Groupe Ka intégrée au site.
45+- **PWA mobile-first** installable + **app iOS SwiftUI 100 % native** (iOS 17+, TestFlight, dépôt séparé `git.spboucher.ai/lou-ka-ios`) avec moteur de recommandation **on-device**.
46+- **Robustesse** — garde-fou **anti-dérive** (`DRIFT_RATIO = 0.25`) : si une source retourne soudainement beaucoup moins d'annonces, les retraits sont **suspendus** au lieu de vider l'inventaire.
47+
48+### Visite guidée
75 49
76 | | | 50 | | |
77 |:---:|:---:| 51 |:---:|:---:|
78−| **Recherche filtrée** — ville, quartier, type (3½…), loyer, animaux, meublé, texte libre | **Vue carte** — Lou-Ka Maps, clusters par prix, « Rechercher dans cette zone » | 52+| **Recherche filtrée** | **Vue carte Lou-Ka Maps** |
79 | <img src="docs/screenshots/annonces.jpg" alt="Grille d'annonces" width="440"> | <img src="docs/screenshots/carte.jpg" alt="Vue carte Lou-Ka Maps" width="440"> | 53 | <img src="docs/screenshots/annonces.jpg" alt="Grille d'annonces" width="440"> | <img src="docs/screenshots/carte.jpg" alt="Vue carte Lou-Ka Maps" width="440"> |
80−| **Fiche complète** — galerie, badge marché, inclusions, quartier, PDF | **Pages villes (SEO)** — `/ville/quebec`, facettes par type, loyers médians | 54+| **Fiche complète** | **Pages villes (SEO)** |
81 | <img src="docs/screenshots/fiche-logement.jpg" alt="Fiche d'un logement" width="440"> | <img src="docs/screenshots/ville-quebec.jpg" alt="Page ville Québec" width="440"> | 55 | <img src="docs/screenshots/fiche-logement.jpg" alt="Fiche d'un logement" width="440"> | <img src="docs/screenshots/ville-quebec.jpg" alt="Page ville Québec" width="440"> |
82−| **Observatoire du marché** — KPI temps réel, loyers par région, rapport PDF | **Registre des sources** — chaque gestionnaire, son statut, sa dernière synchro | 56+| **Observatoire du marché** | **Registre des sources** |
83 | <img src="docs/screenshots/stats.png" alt="Page stats" width="440"> | <img src="docs/screenshots/sources.png" alt="Registre des sources" width="440"> | 57 | <img src="docs/screenshots/stats.png" alt="Page stats" width="440"> | <img src="docs/screenshots/sources.png" alt="Registre des sources" width="440"> |
84 58
85−<div align="center"> 59+## Stack technique
86 60
87−**Mobile-first & PWA installable** 61+| Couche | Technologies | Fichiers |
88−
89−<img src="docs/screenshots/accueil-mobile.png" alt="Version mobile" width="300">
90−
91−</div>
92−
93−## L'architecture en 30 secondes
94−
95−```mermaid
96−flowchart LR
97− subgraph Sources["265 sources recensées"]
98− S1["Gestionnaires : Logisco · Cogir<br/>CAPREIT · Akelius · Mondev …<br/>Portails : Kijiji · LesPAC<br/>RE/MAX · Sutton · Royal LePage"]
99− end
100− subgraph LouKa["Lou-Ka"]
101− C["205 connecteurs<br/><i>1 adaptateur / site</i>"] --> N["Normalisation<br/><i>schéma Listing unique</i>"]
102− N --> T["Text mining<br/><i>digest déterministe</i>"]
103− T --> D[("SQLite<br/>hash + diff")]
104− D --> E["Enrichissements<br/><i>géocodage · POI · quartier</i>"]
105− E --> A["API FastAPI<br/>+ SSR SEO"]
106− A --> F["React 18 + Vite<br/>Lou-Ka Maps · PWA"]
107− A --> I["App iOS<br/>SwiftUI natif"]
108− end
109− W["⏱ Watcher horaire<br/>(PM2)"] -.-> C
110− S1 --> C
111− F --> U["🔑 Locataire"]
112− I --> U
113−```
114−
115−| Couche | Rôle | Fichiers |
116 |---|---|---| 62 |---|---|---|
117−| **Connecteurs** | 1 module Python par source : HTML rendu serveur, API JSON internes (Building Stack, RealVuu, Planpoint, Rentsync, source.immo, JetEngine, WordPress REST…), Firecrawl (Cloudflare) ou Scrapfly (anti-bot) | `louka/connectors/*.py` | 63+| **Backend** | **Python 3.14**, **FastAPI**, uvicorn, **SQLite** (migrations auto, upsert par hash), reportlab/**fpdf2** (PDF) — **8 dépendances** | `louka/` |
118−| **Schéma** | `Listing` standardisé : adresse, secteur, ville, type (3½…), prix, disponibilité, superficie, commodités, **toutes les images** | `louka/schema.py` | 64+| **Connecteurs** | **205 modules auto-découverts** (`pkgutil.iter_modules`) : HTML, API JSON internes (Building Stack, RealVuu, Planpoint, Rentsync, source.immo, WordPress REST…), **Firecrawl** (Cloudflare), **Scrapfly** (anti-bot) | `louka/connectors/*.py` |
119−| **Normalisation** | prix, dates ISO, types d'unités, pi², adresses, commodités canoniques | `louka/normalize.py` | 65+| **Text mining** | digest déterministe des descriptions — regex FR, **< 5 ms/annonce**, **aucun LLM** | `louka/textmine.py` |
120−| **Text mining** | digest structuré des descriptions — règles regex FR, < 5 ms/annonce, **aucun LLM** | `louka/textmine.py` | 66+| **Géocodage** | **Nominatim** (1 req/s) + **Adresses Québec** (MERN ArcGIS, mode **EN LOT** de 200) validé par bounding box provinciale | `louka/geocode.py` |
121−| **Diff engine** | upsert par hash de contenu → nouvelle / modifiée / disparue, avec garde-fou anti-dérive | `louka/db.py` | 67+| **Frontend** | **React 18 + Vite + TypeScript**, design « éditorial sharp » Groupe Ka (Space Grotesk, accent), **PWA** | `frontend/` |
122−| **Enrichissements** | géocodage (Nominatim + Adresses Québec), POI OSM, statistiques de quartier (recensement 2021, INSPQ, SPVM) | `louka/geocode.py` · `poi.py` · `quartier.py` | 68+| **Cartographie** | **`@groupe-ka/ka-maps`** — framework partagé du Groupe Ka, **Mapbox GL JS** style Standard **3D**, monté en `React.lazy` | `frontend/src/kamaps/` |
123−| **API + SEO** | filtres, facettes, GeoJSON, stats, PDF ; SSR léger, sitemaps, JSON-LD, 410/404 | `louka/web.py` · `seo.py` | 69+| **SEO** | SSR léger FastAPI, sitemaps dynamiques, JSON-LD, hreflang, GZip, `/assets` immuable 1 an | `louka/seo.py` |
124−| **Frontend** | design « éditorial sharp » : Space Grotesk, ombres décalées, accent lime, ticker temps réel, PWA | `frontend/` | 70+| **iOS** | **SwiftUI natif** (iOS 17+, zéro dépendance externe), TestFlight | dépôt séparé |
125−| **iOS** | app SwiftUI native (dépôt séparé), moteur de reco on-device | `ios/` | 71+| **Prod** | **PM2** (3 processus) + **ngrok** sur le nœud **M3U96b** | — |
126−
127−## Le pipeline en détail
128−
129−### Connecteurs auto-découverts
130−
131−Le registre parcourt `louka/connectors/` (`pkgutil.iter_modules`) : **déposer un module
132−suffit**, aucun fichier partagé à modifier. `BaseConnector` fournit `get()` (throttlé,
133−poli, User-Agent identifié `LouKaBot`), `get_rendered()` (Firecrawl pour les sites
134−JavaScript/Cloudflare) et `scrapfly()` (anti-bot ASP). Un connecteur qui casse n'affecte
135−jamais les autres : try/except par annonce, journal `sync_log` par source.
136−
137−Dix connecteurs de **portails et bannières de courtage** (Kijiji, LesPAC, RE/MAX,
138−Sutton, Royal LePage, Via Capitale, Proprio Direct, Ubee, Barnes, M Immobilier)
139−complètent les gestionnaires directs, avec limites configurables par variables
140−d'environnement (`LOUKA_*_DETAIL_LIMIT`, `LOUKA_*_MAX_PAGES`).
141−
142−### Diff engine avec garde-fou anti-dérive
143−
144−Chaque annonce est hachée (`content_hash`). À chaque synchro : ajouts, mises à jour et
145−retraits sont déduits du diff. Si une source retourne soudainement beaucoup moins
146−d'annonces que d'habitude (site en panne, HTML changé), le garde-fou
147−(`DRIFT_RATIO = 0.25`) **suspend les retraits** au lieu de vider l'inventaire — la
148−fiabilité d'un agrégateur se joue là. L'historique de prix est conservé dans
149−`price_log` et affiché sur les fiches.
150−
151−### Géocodage sans invention
152−
153−Cache par immeuble (`geocode_cache`), Nominatim (1 req/s) puis **Adresses Québec
154−(MERN ArcGIS)** en repli — y compris un mode **EN LOT** (lots de 200) porté d'Immo-Ka.
155−Chaque résultat est validé contre une bounding box provinciale : hors Québec = rejeté.
156−~70 % des annonces sont géolocalisées ; jamais de coordonnées inventées.
157−
158−### Quartier & proximité
159−
160−- **POI** : commodités à proximité via Overpass/OSM, mutualisées par immeuble
161− (`poi_cache`, clé lat/lng à ~11 m).
162−- **Quartier** : aires de diffusion du recensement 2021 (point-dans-polygone local sur
163− `data/quartier.db`), proximité aux services (PMD StatCan), défavorisation (INSPQ),
164− îlots de chaleur, criminalité (SPVM), écoles (MEQ) — le bloc « Le quartier » de
165− chaque fiche.
166−
167−## Lou-Ka Maps
168−
169−Carte propulsée par **[@groupe-ka/ka-maps](docs/ka-maps-lou-ka.md)** — le framework
170−cartographique partagé du Groupe KA (moteur **Mapbox GL JS**, style Standard **3D**),
171−monté en `React.lazy` :
172−
173−- `GET /api/listings.geojson?bbox=…` piloté par le viewport, requêtes annulables ;
174−- clustering par immeuble avec prix moyen par grappe (`valueClamp [250, 8000]`) ;
175−- « Rechercher en déplaçant la carte », caméra partageable dans l'URL (`?lat&lng&zoom`) ;
176−- bascule 3D/2D, synchronisation liste ↔ carte, compteur « N sur la carte · N hors carte ».
177−
178−## SEO — SSR léger, sitemaps, JSON-LD
179−
180−Le serveur FastAPI (`louka/seo.py`) renvoie **du HTML complet dès la première requête** :
181−même `index.html` que le build Vite, mais avec `<head>` unique (title, description,
182−canonical, hreflang, Open Graph, Twitter) et le contenu essentiel pré-rendu dans
183−`<div id="root">` — React hydrate ensuite.
184−
185−- **Pages programmatiques** : `/villes`, `/ville/{slug}`, `/ville/{slug}/{type}`
186− (« Trois-Rivières » → `trois-rivieres`, « 3½ » → `3-1-2`) ;
187−- **JSON-LD** : `RealEstateListing` + `Apartment` + `Offer` (CAD) + `BreadcrumbList` ;
188−- **Sitemaps dynamiques** : index + pages + villes + annonces (chunks de 10 000) ;
189−- **`410 Gone`** pour les annonces retirées, vrais `404` ailleurs — fin des soft-404 ;
190−- GZip, `/assets` immuable 1 an, HTML `no-cache`.
191−
192−## Comptes, SSO KA & profils
193−
194−- **« Se connecter avec KA »** : SSO via le hub [groupe-ka.com](https://www.groupe-ka.com)
195− (JWT HS256, audience `lou-ka`). Le `ka_id` est **émis par le hub** — source de vérité
196− du groupe ; Google OAuth reste disponible en direct.
197−- Sessions signées HMAC-SHA256 en cookie httpOnly (30 jours), stdlib seulement.
198−- **Locataire** : favoris (poussés vers « Mon univers Ka » du hub), profil carte de
199− membre, page publique opt-in `/u/{ka_id}` — jamais le courriel.
200−- **Gestionnaire** : réclame la page de *sa* source (1:1, premier arrivé), la
201− personnalise, et elle devient publique sur `/g/{source_id}`.
202−
203−## Stats de marché & PDF
204−
205−`louka/marketstats.py` calcule en direct les agrégats du marché (loyer médian/moyen,
206−par région, ville, taille, baisses de prix) — **source unique** pour la page `/stats`,
207−l'API `/api/stats/detailed` et le **rapport de marché PDF** multi-pages
208−(`/api/stats/rapport.pdf`, reportlab). Chaque annonce a aussi sa **fiche PDF**
209−(`/api/listings/{uid}/pdf`) avec QR code vers la fiche en ligne.
210−
211−## App iOS native
212−
213−App **SwiftUI 100 % native** (iOS 17+, zéro dépendance externe), distribuée sur
214−**TestFlight** — dépôt séparé : `git.spboucher.ai/lou-ka-ios`.
215−
216−| Annonces | Découvrir | Carte |
217−|:---:|:---:|:---:|
218−| <img src="docs/screenshots/ios/accueil.jpg" alt="iOS — Annonces" width="240"> | <img src="docs/screenshots/ios/decouverte.jpg" alt="iOS — Découvrir" width="240"> | <img src="docs/screenshots/ios/carte.jpg" alt="iOS — Carte" width="240"> |
219−
220−| Fiche | Stats | Sources |
221−|:---:|:---:|:---:|
222−| <img src="docs/screenshots/ios/fiche.jpg" alt="iOS — Fiche" width="240"> | <img src="docs/screenshots/ios/stats.jpg" alt="iOS — Stats" width="240"> | <img src="docs/screenshots/ios/sources.jpg" alt="iOS — Sources" width="240"> |
223−
224−Le mode **Découvrir** propose un flux d'exploration par balayage avec un **moteur de
225−recommandation on-device** (lissage de Laplace, reclassement toutes les 8 décisions,
226−~1/6 d'exploration) — aucune donnée ne quitte l'appareil.
227 72
228 ## Structure du dépôt 73 ## Structure du dépôt
229 74
230 ``` 75 ```
231 lou-ka/ 76 lou-ka/
232 ├── run.py # point d'entrée CLI (sync / watch / serve / geocode / poi / quartier / record) 77 ├── run.py # point d'entrée CLI (sync / watch / serve / geocode / poi / quartier / record)
233−├── requirements.txt # 8 dépendances backend (FastAPI, uvicorn, bs4, reportlab…) 78+├── requirements.txt # 8 dépendances backend (FastAPI, uvicorn, bs4, reportlab, fpdf2…)
234 ├── louka/ # paquet backend (Python 3.14) 79 ├── louka/ # paquet backend (Python 3.14)
235 │ ├── web.py # app FastAPI : API JSON + service du build Vite 80 │ ├── web.py # app FastAPI : API JSON + service du build Vite
236 │ ├── seo.py # SSR léger, pages programmatiques, sitemaps, robots, JSON-LD 81 │ ├── seo.py # SSR léger, pages programmatiques, sitemaps, robots, JSON-LD
237 │ ├── db.py # SQLite : migrations auto, upsert par hash, anti-dérive 82 │ ├── db.py # SQLite : migrations auto, upsert par hash, anti-dérive
238 │ ├── schema.py # dataclass Listing 83 │ ├── schema.py # dataclass Listing
239−│ ├── normalize.py # normalisation commune (prix, dates, types, adresses) 84+│ ├── normalize.py # normalisation (prix, dates ISO, types d'unités, adresses)
240 │ ├── textmine.py # digest déterministe des descriptions (regex FR) 85 │ ├── textmine.py # digest déterministe des descriptions (regex FR)
241 │ ├── ingest.py # pipeline sync / watch 86 │ ├── ingest.py # pipeline sync / watch
242−│ ├── auth.py # SSO KA + Google OAuth, sessions HMAC 87+│ ├── auth.py # SSO KA + Google OAuth, sessions HMAC-SHA256 (cookie httpOnly 30 j)
243 │ ├── accounts.py # rôles, favoris, réclamation de page gestionnaire 88 │ ├── accounts.py # rôles, favoris, réclamation de page gestionnaire
244−│ ├── profile.py / hubprofile.py / hubfav.py # profils & intégration hub Groupe KA 89+│ ├── profile.py / hubprofile.py / hubfav.py # profils & intégration hub Groupe Ka
245 │ ├── geocode.py # Nominatim + Adresses Québec (mode EN LOT) 90 │ ├── geocode.py # Nominatim + Adresses Québec (mode EN LOT)
246 │ ├── poi.py / quartier.py # commodités OSM, stats de quartier 91 │ ├── poi.py / quartier.py # commodités OSM, stats de quartier
247−│ ├── marketstats.py # agrégats du marché (source unique) 92+│ ├── marketstats.py # agrégats du marché (source unique stats/API/PDF)
248−│ ├── pdfgen.py # fiches PDF + rapport de marché (reportlab, QR) 93+│ ├── pdfgen.py # fiches PDF + rapport de marché (QR code)
249 │ ├── fixtures.py # enregistrement/rejeu HTTP pour tests hors-ligne 94 │ ├── fixtures.py # enregistrement/rejeu HTTP pour tests hors-ligne
250 │ └── connectors/ # 205 connecteurs + base.py (auto-découverte) 95 │ └── connectors/ # 205 connecteurs + base.py (auto-découverte)
251−├── frontend/ # React 18 + Vite + TypeScript 96+├── frontend/ # React 18 + Vite + TypeScript (pages, composants, kamaps/)
252−│ └── src/
253−│ ├── pages/ # Home, Listing, Ville, Stats, Sources, Profil, Favoris…
254−│ ├── components/ # ListingCard, MapView, QuartierBlock, CookieConsent…
255−│ └── kamaps/ # intégration @groupe-ka/ka-maps (Mapbox GL)
256−├── ios/ # app SwiftUI (dépôt séparé, gitignoré ici)
257 ├── data/ 97 ├── data/
258 │ ├── sources.json # registre des 265 sources (statut + raison si non connectable) 98 │ ├── sources.json # registre des 265 sources (statut + raison si non connectable)
259 │ ├── louka.db # base de production (gitignorée) 99 │ ├── louka.db # base de production (gitignorée)
260 │ └── quartier.db # base statique de quartier (gitignorée) 100 │ └── quartier.db # base statique de quartier (gitignorée)
261−├── scripts/ 101+├── scripts/ # build_recensement / build_contexte / build_environnement / merge_quartier / screenshots.mjs
262−│ ├── build_recensement.py # recensement 2021 (StatCan) → staging
263−│ ├── build_contexte.py # proximité PMD, défavorisation INSPQ, écoles MEQ
264−│ ├── build_environnement.py # îlots de chaleur, criminalité SPVM
265−│ ├── merge_quartier.py # fusion → data/quartier.db
266−│ └── screenshots.mjs # captures du site prod pour ce README (Playwright)
267 ├── tests/ # pytest + fixtures HTTP rejouables hors-ligne 102 ├── tests/ # pytest + fixtures HTTP rejouables hors-ligne
268−├── reports/ # audits : 195 fiches connecteurs, expansion par région, quartier… 103+├── reports/ # audits : fiches connecteurs, expansion par région, quartier…
269−└── docs/ # ka-maps-lou-ka.md + screenshots/ 104+└── docs/ # screenshot.png, screenshots/, ka-maps-lou-ka.md
270 ``` 105 ```
271 106
272−## Démarrage rapide 107+## Démarrage local
273 108
274 ```bash 109 ```bash
275 git clone https://git.spboucher.ai/lou-ka.git && cd lou-ka 110 git clone https://git.spboucher.ai/lou-ka.git && cd lou-ka
276−# miroir : https://github.com/spboucher-ai/lou-ka.git
277 111
278 # Backend 112 # Backend
279 python3 -m venv .venv && .venv/bin/pip install -r requirements.txt 113 python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
@@ -293,179 +127,101 @@ EOF
293 .venv/bin/python run.py watch 60 # resynchronisation en boucle (minutes) 127 .venv/bin/python run.py watch 60 # resynchronisation en boucle (minutes)
294 ``` 128 ```
295 129
296−En développement frontend : `cd frontend && npm run dev` (Vite sur `:5173`, 130+En développement frontend : `cd frontend && npm run dev` (**Vite sur `:5173`**, proxy `/api` → `:8080`).
297−proxy `/api` → `:8080`).
298 131
299−## CLI `run.py` 132+### CLI `run.py`
300 133
301 | Commande | Rôle | 134 | Commande | Rôle |
302 |---|---| 135 |---|---|
303−| `run.py sync [source ...]` | ingestion — toutes les sources ou une liste | 136+| **`run.py sync [source ...]`** | ingestion — toutes les sources ou une liste |
304−| `run.py watch [minutes]` | boucle de resynchronisation (défaut 60) | 137+| **`run.py watch [minutes]`** | boucle de resynchronisation (défaut **60**) |
305−| `run.py serve [port]` | uvicorn `louka.web:app` (défaut 8080) | 138+| **`run.py serve [port]`** | uvicorn `louka.web:app` (défaut **8080**) |
306−| `run.py geocode [n]` | géocodage **en lot** (Adresses Québec, lots de 200) | 139+| **`run.py geocode [n]`** | géocodage **en lot** (Adresses Québec, lots de 200) |
307−| `run.py geocode1 [n]` | géocodage un-par-un (Nominatim, 1 req/s) | 140+| **`run.py geocode1 [n]`** | géocodage un-par-un (Nominatim, 1 req/s) |
308−| `run.py poi [n]` | commodités OSM à proximité (Overpass) | 141+| **`run.py poi [n]`** | commodités OSM à proximité (Overpass) |
309−| `run.py quartier [n]` | enrichissement quartier (recensement, INSPQ, SPVM) | 142+| **`run.py quartier [n]`** | enrichissement quartier (recensement, INSPQ, SPVM) |
310−| `run.py record <source>` | (ré)enregistre les fixtures HTTP d'un connecteur | 143+| **`run.py record <source>`** | (ré)enregistre les fixtures HTTP d'un connecteur |
311 144
312−## Variables d'environnement 145+### Variables d'environnement
313 146
314−`run.py` charge `.env` à la racine (parsing maison, aucune dépendance). 147+`run.py` charge **`.env`** à la racine (parsing maison, aucune dépendance).
315 148
316 | Variable | Rôle | 149 | Variable | Rôle |
317 |---|---| 150 |---|---|
318−| `FIRECRAWL_API_KEY` | rendu JavaScript / contournement Cloudflare (optionnel) | 151+| **`FIRECRAWL_API_KEY`** | rendu JavaScript / contournement Cloudflare (optionnel) |
319−| `SCRAPFLY_KEY` | scraping anti-bot ASP (optionnel) | 152+| **`SCRAPFLY_KEY`** | scraping anti-bot ASP (optionnel) |
320−| `SESSION_SECRET` | signature HMAC des sessions | 153+| **`SESSION_SECRET`** | signature HMAC des sessions |
321−| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | connexion Google OAuth | 154+| **`GOOGLE_CLIENT_ID`** / **`GOOGLE_CLIENT_SECRET`** | connexion Google OAuth |
322−| `KA_SSO_SECRET` | secret partagé du SSO KA (JWT HS256) | 155+| **`KA_SSO_SECRET`** | secret partagé du SSO KA (JWT HS256) |
323−| `KA_HUB_URL` | hub Groupe KA (défaut `https://www.groupe-ka.com`) | 156+| **`KA_HUB_URL`** | hub Groupe Ka (défaut `https://www.groupe-ka.com`) |
324−| `LOUKA_BASE_URL` | URL publique (derrière ngrok/proxy) | 157+| **`LOUKA_BASE_URL`** | URL publique (derrière ngrok/proxy) |
325 | `LOUKA_*_DETAIL_LIMIT` / `LOUKA_*_MAX_PAGES` | limites par portail (Kijiji, LesPAC, RE/MAX…) | 158 | `LOUKA_*_DETAIL_LIMIT` / `LOUKA_*_MAX_PAGES` | limites par portail (Kijiji, LesPAC, RE/MAX…) |
326−| `VITE_MAPBOX_TOKEN` | jeton Mapbox du frontend (repli public codé en dur) | 159+| **`VITE_MAPBOX_TOKEN`** | jeton Mapbox du frontend (repli public codé en dur) |
327−
328−## API
329−
330−### Annonces & recherche
331 160
332−| Endpoint | Description | 161+### Tests
333−|---|---|
334−| `GET /api/listings` | recherche filtrée : `city, sector, unit_type, source, price_min/max, pets, furnished, available_by, area_min, q, limit, offset` |
335−| `GET /api/listings.geojson?bbox=O,S,E,N` | FeatureCollection pour la carte (+ `totalGeocoded`, `totalMatching`) |
336−| `GET /api/listings/{uid}` | fiche complète : images, POI, quartier, digest, historique de prix |
337−| `GET /api/listings/{uid}/pdf` | fiche de propriété PDF (QR code) |
338−| `GET /api/facets` | villes, quartiers (`?city=`), types, sources — pour construire les filtres |
339 162
340−### Marché & registre 163+```bash
164+.venv/bin/python -m pytest
165+```
341 166
342−| Endpoint | Description | 167+Chaque connecteur est rejoué **hors-ligne** contre ses fixtures HTTP enregistrées (`tests/fixtures/<source>/` + `expected.json`) ; la normalisation et le text mining ont leurs suites dédiées.
343−|---|---|
344−| `GET /api/stats` | totaux Québec / Lévis / Grand Montréal / autres, loyer moyen, dernières synchros |
345−| `GET /api/stats/detailed` | agrégats complets (régions, villes, tailles, baisses de prix) |
346−| `GET /api/stats/rapport.pdf` | rapport de marché PDF multi-pages |
347−| `GET /api/sources` | registre des 265 sources + compteurs + dernière synchro |
348−| `POST /api/sync` | déclenche une synchronisation en arrière-plan (`?source=`) |
349 168
350−### Comptes & profils 169+## API (aperçu)
351 170
352 | Endpoint | Description | 171 | Endpoint | Description |
353 |---|---| 172 |---|---|
354−| `GET /api/auth/ka/login` → `/callback` | SSO KA (hub groupe-ka.com) | 173+| **`GET /api/listings`** | recherche filtrée : `city, sector, unit_type, source, price_min/max, pets, furnished, available_by, area_min, q, limit, offset` |
355−| `GET /api/auth/google/login` → `/callback` | Google OAuth direct | 174+| **`GET /api/listings.geojson?bbox=O,S,E,N`** | FeatureCollection pour la carte (+ `totalGeocoded`, `totalMatching`) |
356−| `GET /api/me` · `POST /api/me/role` · `PUT /api/me/profile` · `POST /api/me/avatar` | session & profil | 175+| **`GET /api/listings/{uid}`** | fiche complète : images, POI, quartier, digest, historique de prix |
357−| `GET/POST/DELETE /api/favorites[/{uid}]` | favoris (synchronisés vers le hub KA) | 176+| **`GET /api/listings/{uid}/pdf`** | fiche de propriété PDF (QR code) |
358−| `POST /api/org/claim` · `PUT /api/org` · `GET /api/org/{source_id}` | pages gestionnaires | 177+| **`GET /api/facets`** | villes, quartiers, types, sources — pour construire les filtres |
359−| `GET /api/users/{ka_id}` | profil public opt-in | 178+| **`GET /api/stats`** / **`/api/stats/detailed`** / **`/api/stats/rapport.pdf`** | agrégats du marché + rapport PDF |
360− 179+| **`GET /api/sources`** | registre des **265 sources** + compteurs + dernière synchro |
361−### Pages HTML servies par le SSR (`louka/seo.py`) 180+| **`POST /api/sync`** | synchronisation en arrière-plan (`?source=`) |
362− 181+| **`GET /api/auth/ka/login`** / **`/api/auth/google/login`** | SSO KA & Google OAuth |
363−`/` · `/villes` · `/ville/{slug}[/{type}]` · `/logement/{uid}` (410 si retirée) · 182+| `GET /api/me` · `/api/favorites` · `/api/org/*` · `/api/users/{ka_id}` | session, favoris, pages gestionnaires, profils publics |
364−`/g/{source}` · `/stats` · `/sources` · `/robots.txt` · `/sitemap*.xml`
365−
366−## Ajouter un gestionnaire (≈ 30 lignes)
367−
368−L'enregistrement est **auto-découvrant** : déposez un module dans `louka/connectors/`,
369−c'est tout.
370−
371−```python
372−# louka/connectors/mon_agence.py
373−from ..schema import Listing, infer_city, normalize_unit_type, parse_price
374−from .base import BaseConnector
375−
376−class MonAgenceConnector(BaseConnector):
377− source_id = "mon_agence"
378−
379− def fetch(self) -> list[Listing]:
380− html = self.get("https://mon-agence.ca/logements").text # throttlé, poli
381− # ... parser les cartes, les fiches, les photos ...
382− return [Listing(
383− source=self.source_id, external_id="123",
384− url="https://mon-agence.ca/logement/123",
385− title="555, avenue Exemple", sector="Limoilou",
386− city=infer_city("Limoilou"), unit_type=normalize_unit_type("4 1/2"),
387− price=parse_price("1 250 $ / mois"), images=[...],
388− )]
389−```
390 183
391−Puis : `.venv/bin/python run.py sync mon_agence` — l'annonce apparaît sur le site, 184+Pages HTML servies par le SSR (`louka/seo.py`) : `/` · `/villes` · `/ville/{slug}[/{type}]` · `/logement/{uid}` (**410** si retirée) · `/g/{source}` · `/stats` · `/sources` · `/robots.txt` · `/sitemap*.xml`
392−avec sa fiche, sa galerie et son lien source. Ajoutez l'entrée correspondante dans
393−`data/sources.json` pour la page **Sources**, et `run.py record mon_agence` pour
394−figer ses fixtures de test.
395 185
396−## Tests 186+## Déploiement
397 187
398−```bash 188+En production sur le nœud **M3U96b** (Mac Studio, cluster MacLustr), dans **`~/apps/lou-ka`**, sous **PM2** (**3 processus**) derrière **ngrok** :
399−.venv/bin/python -m pytest
400−```
401−
402−- `tests/test_connectors.py` — chaque connecteur rejoué **hors-ligne** contre ses
403− fixtures HTTP enregistrées (`tests/fixtures/<source>/` + `expected.json`) ;
404−- `tests/test_normalize.py` — normalisation (prix, dates, types, adresses) ;
405−- `tests/test_textmine.py` — digest des descriptions.
406 189
407−## Couverture 190+| Processus PM2 | Commande | Rôle |
191+|---|---|---|
192+| **`lou-ka-web`** | `.venv/bin/python run.py serve 8095` | API + SSR + frontend sur le **port 8095** |
193+| **`lou-ka-sync`** | `.venv/bin/python run.py watch 60` | resynchronisation **horaire** des sources |
194+| **`lou-ka-ngrok`** | `ngrok http --url=www.lou-ka.com 8095` | tunnel → **https://www.lou-ka.com** |
408 195
409−**11 régions, 820 villes.** Grand Montréal & environs, Québec métro & Lévis, 196+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**).
410−Outaouais, Estrie / Montérégie-Est, Mauricie / Centre-du-Québec, Lanaudière /
411−Laurentides, Saguenay–Lac-Saint-Jean, Chaudière-Appalaches, Bas-Saint-Laurent,
412−Abitibi, Côte-Nord & Gaspésie.
413 197
414−Côté sources : des gestionnaires directs (Logisco, Cogir, CAPREIT, Akelius, Trudel, 198+## Développement remote-first (IMPORTANT)
415−Immostar, Groupe Dallaire, Mondev, Devimco…), des dizaines de complexes, et les grands
416−portails (Kijiji, LesPAC) + bannières de courtage (RE/MAX, Sutton, Royal LePage,
417−Via Capitale, Proprio Direct…). Chaque source **non connectable est documentée avec sa
418−raison** dans `data/sources.json` (aucun prix affiché, inventaire vide, site
419−placeholder…), et chaque connecteur a sa fiche d'audit dans `reports/connectors/`.
420 199
421−## Production 200+La **source de vérité est le repo git sur le nœud M3U96b** (`~/apps/lou-ka`), **pas** une copie locale sur le laptop. Toute modification se fait **sur le nœud via SSH** : édition, build frontend, `pm2 restart lou-ka-web`, puis commit/push **depuis le nœud**.
422 201
423−Déployé sous **PM2** (3 processus) derrière **ngrok** : 202+- Remote **`origin` = spbgit** — le serveur git personnel **git.spboucher.ai** (bare repo `~/srv/git/lou-ka.git` sur M3U96a, alias SSH `gitsrv`). **Pas GitHub.**
424− 203+- Le push fonctionne grâce à l'**agent forwarding SSH** actif pendant une session depuis le laptop.
425−```
426−lou-ka-web .venv/bin/python run.py serve 8095 # API + SSR + frontend
427−lou-ka-sync .venv/bin/python run.py watch 60 # resync horaire
428−lou-ka-ngrok ngrok http --url=www.lou-ka.com 8095 # tunnel
429−```
430−
431−Philosophie d'exploitation : **on ne pousse que le code — le serveur maintient ses
432−données lui-même** (`data/*.db`, `frontend/dist` et `.env` ne sont jamais versionnés).
433 204
434 ## Principes 205 ## Principes
435 206
436−1. **Politesse** — délai ≥ 0,5 s entre requêtes, garde-fous de crawl, User-Agent 207+1. **Politesse** — délai **≥ 0,5 s** entre requêtes, garde-fous de crawl, User-Agent identifié (**`LouKaBot`**, page de transparence `/bot`).
437− identifié (`LouKaBot`, page de transparence `/bot`). 208+2. **Fidélité** — **aucun prix inventé** (`price = null` si absent), **aucune coordonnée devinée** (géocodage validé ou rien).
438−2. **Fidélité** — aucun prix inventé : si la source n'affiche pas de prix, 209+3. **Traçabilité** — chaque fiche renvoie vers l'annonce originale via **`/passerelle/{uid}`**.
439− `price = null` ; aucune coordonnée devinée : géocodage validé ou rien. 210+4. **Robustesse** — un connecteur qui casse n'affecte jamais les autres ; le **garde-fou anti-dérive** suspend les retraits quand une source déraille.
440−3. **Traçabilité** — chaque fiche renvoie vers l'annonce originale du gestionnaire 211+5. **Vie privée** — **aucun traceur tiers**, consentement honnête, profil public strictement **opt-in**.
441− (passerelle de sortie `/passerelle/{uid}`).
442−4. **Robustesse** — un connecteur qui casse n'affecte jamais les autres ; le
443− garde-fou anti-dérive suspend les retraits quand une source déraille.
444−5. **Respect de la vie privée** — aucun traceur tiers, consentement honnête,
445− profil public strictement opt-in.
446 212
447−## Écosystème Groupe KA 213+## Écosystème Groupe Ka
448 214
449−Lou-Ka est une plateforme du **[Groupe KA](https://www.groupe-ka.com)**, aux côtés 215+Lou-Ka fait partie des plateformes du **[Groupe Ka](https://www.groupe-ka.com)**, aux côtés d'**Immo-Ka** (propriétés à vendre), **Vrai-Prix** et les autres univers ·Ka — avec en partage : le **SSO KA** (`ka_id` émis par le hub), le framework cartographique **Ka Maps**, le design system « **éditorial sharp** » et le géocodage **Adresses Québec**.
450−d'**Immo-Ka** (propriétés à vendre) et **Vrai-Prix** — avec en partage : le SSO KA
451−(`ka_id` émis par le hub), le framework cartographique **Ka Maps**, et le géocodage
452−Adresses Québec.
453 216
454 --- 217 ---
455 218
456 <div align="center"> 219 <div align="center">
457 220
458−## Auteur 221+**Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai) · [git.spboucher.ai](https://git.spboucher.ai)
459−
460−**Simon-Pierre Boucher**
461−
462−[![Email](https://img.shields.io/badge/contact@spboucher.ai-141814?style=for-the-badge&logo=minutemailer&logoColor=d9f26b)](mailto:contact@spboucher.ai)
463−[![Git](https://img.shields.io/badge/git.spboucher.ai-141814?style=for-the-badge&logo=git&logoColor=d9f26b)](https://git.spboucher.ai)
464−[![GitHub](https://img.shields.io/badge/spboucher--ai-141814?style=for-the-badge&logo=github&logoColor=d9f26b)](https://github.com/spboucher-ai)
465 222
466−*Du recensement de marché (~240 gestionnaires vérifiés ville par ville) au produit en 223+© 2026 — tous droits réservés.
467−production — 22 900+ annonces, 205 connecteurs, SSR SEO, cartes 3D et app iOS.*
468 224
469−© 2026 Simon-Pierre Boucher — tous droits réservés. 225+Un service **Groupe Ka**
470 226
471 </div> 227 </div>
added docs/screenshot.png +0 −0

Binary file not shown.

Menu

Theme