docs: README à jour avec screenshot
2 changed files +106 −350
modified
README.md
+106 −350
@@ -1,10 +1,10 @@ | ||
| 1 | −<div align="center"> | |
| 2 | − | |
| 3 | 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 | + | |
| 8 | 8 | |
| 9 | 9 |  |
| 10 | 10 |  |
@@ -20,260 +20,94 @@ | ||
| 20 | 20 |  |
| 21 | 21 |  |
| 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 | |
| 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) | |
| 25 | +## Description | |
| 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 — | |
| 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. | |
| 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). | |
| 64 | 32 | |
| 65 | −> Les sites d'agences n'offrent pas de webhooks. Lou-Ka reproduit l'équivalent : | |
| 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). | |
| 33 | +**En chiffres** : **22 900+ annonces actives**, **265 sources recensées**, **205 connecteurs**, **820 villes**, **11 régions**, **~70 %** des annonces géolocalisées. | |
| 69 | 34 | |
| 70 | −Lou-Ka n'est pas une plateforme d'annonces : c'est un **index fidèle**. Aucun prix | |
| 71 | −inventé, aucune coordonnée devinée, et chaque fiche renvoie vers l'annonce originale | |
| 72 | −du gestionnaire via une passerelle de sortie transparente. | |
| 35 | +## Fonctionnalités | |
| 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 | 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 | 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 | 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** | |
| 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 | | |
| 61 | +| Couche | Technologies | 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` | | |
| 118 | −| **Schéma** | `Listing` standardisé : adresse, secteur, ville, type (3½…), prix, disponibilité, superficie, commodités, **toutes les images** | `louka/schema.py` | | |
| 119 | −| **Normalisation** | prix, dates ISO, types d'unités, pi², adresses, commodités canoniques | `louka/normalize.py` | | |
| 120 | −| **Text mining** | digest structuré des descriptions — règles regex FR, < 5 ms/annonce, **aucun LLM** | `louka/textmine.py` | | |
| 121 | −| **Diff engine** | upsert par hash de contenu → nouvelle / modifiée / disparue, avec garde-fou anti-dérive | `louka/db.py` | | |
| 122 | −| **Enrichissements** | géocodage (Nominatim + Adresses Québec), POI OSM, statistiques de quartier (recensement 2021, INSPQ, SPVM) | `louka/geocode.py` · `poi.py` · `quartier.py` | | |
| 123 | −| **API + SEO** | filtres, facettes, GeoJSON, stats, PDF ; SSR léger, sitemaps, JSON-LD, 410/404 | `louka/web.py` · `seo.py` | | |
| 124 | −| **Frontend** | design « éditorial sharp » : Space Grotesk, ombres décalées, accent lime, ticker temps réel, PWA | `frontend/` | | |
| 125 | −| **iOS** | app SwiftUI native (dépôt séparé), moteur de reco on-device | `ios/` | | |
| 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. | |
| 63 | +| **Backend** | **Python 3.14**, **FastAPI**, uvicorn, **SQLite** (migrations auto, upsert par hash), reportlab/**fpdf2** (PDF) — **8 dépendances** | `louka/` | | |
| 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` | | |
| 65 | +| **Text mining** | digest déterministe des descriptions — 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` | | |
| 67 | +| **Frontend** | **React 18 + Vite + TypeScript**, design « éditorial sharp » Groupe Ka (Space Grotesk, accent), **PWA** | `frontend/` | | |
| 68 | +| **Cartographie** | **`@groupe-ka/ka-maps`** — framework partagé du Groupe Ka, **Mapbox GL JS** style Standard **3D**, monté en `React.lazy` | `frontend/src/kamaps/` | | |
| 69 | +| **SEO** | SSR léger FastAPI, sitemaps dynamiques, JSON-LD, hreflang, GZip, `/assets` immuable 1 an | `louka/seo.py` | | |
| 70 | +| **iOS** | **SwiftUI natif** (iOS 17+, zéro dépendance externe), TestFlight | dépôt séparé | | |
| 71 | +| **Prod** | **PM2** (3 processus) + **ngrok** sur le nœud **M3U96b** | — | | |
| 227 | 72 | |
| 228 | 73 | ## Structure du dépôt |
| 229 | 74 | |
| 230 | 75 | ``` |
| 231 | 76 | lou-ka/ |
| 232 | 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 | 79 | ├── louka/ # paquet backend (Python 3.14) |
| 235 | 80 | │ ├── web.py # app FastAPI : API JSON + service du build Vite |
| 236 | 81 | │ ├── seo.py # SSR léger, pages programmatiques, sitemaps, robots, JSON-LD |
| 237 | 82 | │ ├── db.py # SQLite : migrations auto, upsert par hash, anti-dérive |
| 238 | 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 | 85 | │ ├── textmine.py # digest déterministe des descriptions (regex FR) |
| 241 | 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 | 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 | 90 | │ ├── geocode.py # Nominatim + Adresses Québec (mode EN LOT) |
| 246 | 91 | │ ├── poi.py / quartier.py # commodités OSM, stats de quartier |
| 247 | −│ ├── marketstats.py # agrégats du marché (source unique) | |
| 248 | −│ ├── pdfgen.py # fiches PDF + rapport de marché (reportlab, QR) | |
| 92 | +│ ├── marketstats.py # agrégats du marché (source unique stats/API/PDF) | |
| 93 | +│ ├── pdfgen.py # fiches PDF + rapport de marché (QR code) | |
| 249 | 94 | │ ├── fixtures.py # enregistrement/rejeu HTTP pour tests hors-ligne |
| 250 | 95 | │ └── connectors/ # 205 connecteurs + base.py (auto-découverte) |
| 251 | −├── frontend/ # React 18 + Vite + TypeScript | |
| 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) | |
| 96 | +├── frontend/ # React 18 + Vite + TypeScript (pages, composants, kamaps/) | |
| 257 | 97 | ├── data/ |
| 258 | 98 | │ ├── sources.json # registre des 265 sources (statut + raison si non connectable) |
| 259 | 99 | │ ├── louka.db # base de production (gitignorée) |
| 260 | 100 | │ └── quartier.db # base statique de quartier (gitignorée) |
| 261 | −├── scripts/ | |
| 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) | |
| 101 | +├── scripts/ # build_recensement / build_contexte / build_environnement / merge_quartier / screenshots.mjs | |
| 267 | 102 | ├── tests/ # pytest + fixtures HTTP rejouables hors-ligne |
| 268 | −├── reports/ # audits : 195 fiches connecteurs, expansion par région, quartier… | |
| 269 | −└── docs/ # ka-maps-lou-ka.md + screenshots/ | |
| 103 | +├── reports/ # audits : fiches connecteurs, expansion par région, quartier… | |
| 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 | 109 | ```bash |
| 275 | 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 | 112 | # Backend |
| 279 | 113 | python3 -m venv .venv && .venv/bin/pip install -r requirements.txt |
@@ -293,179 +127,101 @@ EOF | ||
| 293 | 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`, | |
| 297 | −proxy `/api` → `:8080`). | |
| 130 | +En développement frontend : `cd frontend && npm run dev` (**Vite sur `:5173`**, proxy `/api` → `:8080`). | |
| 298 | 131 | |
| 299 | −## CLI `run.py` | |
| 132 | +### CLI `run.py` | |
| 300 | 133 | |
| 301 | 134 | | Commande | Rôle | |
| 302 | 135 | |---|---| |
| 303 | −| `run.py sync [source ...]` | ingestion — toutes les sources ou une liste | | |
| 304 | −| `run.py watch [minutes]` | boucle de resynchronisation (défaut 60) | | |
| 305 | −| `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) | | |
| 307 | −| `run.py geocode1 [n]` | géocodage un-par-un (Nominatim, 1 req/s) | | |
| 308 | −| `run.py poi [n]` | commodités OSM à proximité (Overpass) | | |
| 309 | −| `run.py quartier [n]` | enrichissement quartier (recensement, INSPQ, SPVM) | | |
| 310 | −| `run.py record <source>` | (ré)enregistre les fixtures HTTP d'un connecteur | | |
| 136 | +| **`run.py sync [source ...]`** | ingestion — toutes les sources ou une liste | | |
| 137 | +| **`run.py watch [minutes]`** | boucle de resynchronisation (défaut **60**) | | |
| 138 | +| **`run.py serve [port]`** | uvicorn `louka.web:app` (défaut **8080**) | | |
| 139 | +| **`run.py geocode [n]`** | géocodage **en lot** (Adresses Québec, lots de 200) | | |
| 140 | +| **`run.py geocode1 [n]`** | géocodage un-par-un (Nominatim, 1 req/s) | | |
| 141 | +| **`run.py poi [n]`** | commodités OSM à proximité (Overpass) | | |
| 142 | +| **`run.py quartier [n]`** | enrichissement quartier (recensement, INSPQ, SPVM) | | |
| 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 | 149 | | Variable | Rôle | |
| 317 | 150 | |---|---| |
| 318 | −| `FIRECRAWL_API_KEY` | rendu JavaScript / contournement Cloudflare (optionnel) | | |
| 319 | −| `SCRAPFLY_KEY` | scraping anti-bot ASP (optionnel) | | |
| 320 | −| `SESSION_SECRET` | signature HMAC des sessions | | |
| 321 | −| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | connexion Google OAuth | | |
| 322 | −| `KA_SSO_SECRET` | secret partagé du SSO KA (JWT HS256) | | |
| 323 | −| `KA_HUB_URL` | hub Groupe KA (défaut `https://www.groupe-ka.com`) | | |
| 324 | −| `LOUKA_BASE_URL` | URL publique (derrière ngrok/proxy) | | |
| 151 | +| **`FIRECRAWL_API_KEY`** | rendu JavaScript / contournement Cloudflare (optionnel) | | |
| 152 | +| **`SCRAPFLY_KEY`** | scraping anti-bot ASP (optionnel) | | |
| 153 | +| **`SESSION_SECRET`** | signature HMAC des sessions | | |
| 154 | +| **`GOOGLE_CLIENT_ID`** / **`GOOGLE_CLIENT_SECRET`** | connexion Google OAuth | | |
| 155 | +| **`KA_SSO_SECRET`** | secret partagé du SSO KA (JWT HS256) | | |
| 156 | +| **`KA_HUB_URL`** | hub Groupe Ka (défaut `https://www.groupe-ka.com`) | | |
| 157 | +| **`LOUKA_BASE_URL`** | URL publique (derrière ngrok/proxy) | | |
| 325 | 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) | | |
| 327 | − | |
| 328 | −## API | |
| 329 | − | |
| 330 | −### Annonces & recherche | |
| 159 | +| **`VITE_MAPBOX_TOKEN`** | jeton Mapbox du frontend (repli public codé en dur) | | |
| 331 | 160 | |
| 332 | −| Endpoint | Description | | |
| 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 | | |
| 161 | +### Tests | |
| 339 | 162 | |
| 340 | −### Marché & registre | |
| 163 | +```bash | |
| 164 | +.venv/bin/python -m pytest | |
| 165 | +``` | |
| 341 | 166 | |
| 342 | −| Endpoint | Description | | |
| 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=`) | | |
| 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. | |
| 349 | 168 | |
| 350 | −### Comptes & profils | |
| 169 | +## API (aperçu) | |
| 351 | 170 | |
| 352 | 171 | | Endpoint | Description | |
| 353 | 172 | |---|---| |
| 354 | −| `GET /api/auth/ka/login` → `/callback` | SSO KA (hub groupe-ka.com) | | |
| 355 | −| `GET /api/auth/google/login` → `/callback` | Google OAuth direct | | |
| 356 | −| `GET /api/me` · `POST /api/me/role` · `PUT /api/me/profile` · `POST /api/me/avatar` | session & profil | | |
| 357 | −| `GET/POST/DELETE /api/favorites[/{uid}]` | favoris (synchronisés vers le hub KA) | | |
| 358 | −| `POST /api/org/claim` · `PUT /api/org` · `GET /api/org/{source_id}` | pages gestionnaires | | |
| 359 | −| `GET /api/users/{ka_id}` | profil public opt-in | | |
| 360 | − | |
| 361 | −### Pages HTML servies par le SSR (`louka/seo.py`) | |
| 362 | − | |
| 363 | −`/` · `/villes` · `/ville/{slug}[/{type}]` · `/logement/{uid}` (410 si retirée) · | |
| 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 | −``` | |
| 173 | +| **`GET /api/listings`** | recherche filtrée : `city, sector, unit_type, source, price_min/max, pets, furnished, available_by, area_min, q, limit, offset` | | |
| 174 | +| **`GET /api/listings.geojson?bbox=O,S,E,N`** | FeatureCollection pour la carte (+ `totalGeocoded`, `totalMatching`) | | |
| 175 | +| **`GET /api/listings/{uid}`** | fiche complète : images, POI, quartier, digest, historique de prix | | |
| 176 | +| **`GET /api/listings/{uid}/pdf`** | fiche de propriété PDF (QR code) | | |
| 177 | +| **`GET /api/facets`** | villes, quartiers, types, sources — pour construire les filtres | | |
| 178 | +| **`GET /api/stats`** / **`/api/stats/detailed`** / **`/api/stats/rapport.pdf`** | agrégats du marché + rapport PDF | | |
| 179 | +| **`GET /api/sources`** | registre des **265 sources** + compteurs + dernière synchro | | |
| 180 | +| **`POST /api/sync`** | synchronisation en arrière-plan (`?source=`) | | |
| 181 | +| **`GET /api/auth/ka/login`** / **`/api/auth/google/login`** | SSO KA & Google OAuth | | |
| 182 | +| `GET /api/me` · `/api/favorites` · `/api/org/*` · `/api/users/{ka_id}` | session, favoris, pages gestionnaires, profils publics | | |
| 390 | 183 | |
| 391 | −Puis : `.venv/bin/python run.py sync mon_agence` — l'annonce apparaît sur le site, | |
| 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. | |
| 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` | |
| 395 | 185 | |
| 396 | −## Tests | |
| 186 | +## Déploiement | |
| 397 | 187 | |
| 398 | −```bash | |
| 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. | |
| 188 | +En production sur le nœud **M3U96b** (Mac Studio, cluster MacLustr), dans **`~/apps/lou-ka`**, sous **PM2** (**3 processus**) derrière **ngrok** : | |
| 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, | |
| 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. | |
| 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**). | |
| 413 | 197 | |
| 414 | −Côté sources : des gestionnaires directs (Logisco, Cogir, CAPREIT, Akelius, Trudel, | |
| 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/`. | |
| 198 | +## Développement remote-first (IMPORTANT) | |
| 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** : | |
| 424 | − | |
| 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). | |
| 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.** | |
| 203 | +- Le push fonctionne grâce à l'**agent forwarding SSH** actif pendant une session depuis le laptop. | |
| 433 | 204 | |
| 434 | 205 | ## Principes |
| 435 | 206 | |
| 436 | −1. **Politesse** — délai ≥ 0,5 s entre requêtes, garde-fous de crawl, User-Agent | |
| 437 | − identifié (`LouKaBot`, page de transparence `/bot`). | |
| 438 | −2. **Fidélité** — aucun prix inventé : si la source n'affiche pas de prix, | |
| 439 | − `price = null` ; aucune coordonnée devinée : géocodage validé ou rien. | |
| 440 | −3. **Traçabilité** — chaque fiche renvoie vers l'annonce originale du gestionnaire | |
| 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. | |
| 207 | +1. **Politesse** — délai **≥ 0,5 s** entre requêtes, garde-fous de crawl, User-Agent 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). | |
| 209 | +3. **Traçabilité** — chaque fiche renvoie vers l'annonce originale via **`/passerelle/{uid}`**. | |
| 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. | |
| 211 | +5. **Vie privée** — **aucun traceur tiers**, consentement honnête, 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 | |
| 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. | |
| 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**. | |
| 453 | 216 | |
| 454 | 217 | --- |
| 455 | 218 | |
| 456 | 219 | <div align="center"> |
| 457 | 220 | |
| 458 | −## Auteur | |
| 459 | − | |
| 460 | −**Simon-Pierre Boucher** | |
| 461 | − | |
| 462 | −[](mailto:contact@spboucher.ai) | |
| 463 | −[](https://git.spboucher.ai) | |
| 464 | −[](https://github.com/spboucher-ai) | |
| 221 | +**Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai) · [git.spboucher.ai](https://git.spboucher.ai) | |
| 465 | 222 | |
| 466 | −*Du recensement de marché (~240 gestionnaires vérifiés ville par ville) au produit en | |
| 467 | −production — 22 900+ annonces, 205 connecteurs, SSR SEO, cartes 3D et app iOS.* | |
| 223 | +© 2026 — tous droits réservés. | |
| 468 | 224 | |
| 469 | −© 2026 Simon-Pierre Boucher — tous droits réservés. | |
| 225 | +Un service **Groupe Ka** | |
| 470 | 226 | |
| 471 | 227 | </div> |
added
docs/screenshot.png
+0 −0
Binary file not shown.