{"type":"user_prompt","text":"# MISSION — TRANSFORMER LES FICHES LOU-KA EN DOSSIERS D'INTELLIGENCE LOCATIVE\n\nTu travailles directement dans le projet existant **Lou-Ka (lou-ka.com)**.\n\nLou-Ka est un agrégateur québécois de logements locatifs. Le produit possède déjà des fiches enrichies avec notamment :\n\n* informations de l'annonce;\n* photos;\n* prix;\n* superficie;\n* inclusions;\n* commodités;\n* analyse de juste valeur;\n* comparables;\n* distribution des loyers;\n* Registre des loyers;\n* historique TAL;\n* risque d'inondation;\n* qualité de l'air;\n* commerces;\n* transport;\n* essence;\n* KA Scores;\n* données démographiques;\n* services;\n* criminalité;\n* îlots de chaleur;\n* proximité.\n\n## OBJECTIF\n\nFaire un **upgrade majeur**, mais propre et cohérent, de la fiche logement.\n\nJe ne veux PAS simplement ajouter une dizaine de widgets décoratifs.\n\nJe veux transformer Lou-Ka en un système capable de répondre à :\n\n> « Qu'est-ce que je devrais savoir sur ce logement, cet immeuble, son gestionnaire et son environnement avant de signer un bail? »\n\nLa fiche doit devenir un véritable **passeport numérique du logement et de l'immeuble**.\n\n---\n\n# 1. COMMENCER PAR AUDITER LE PROJET\n\nAvant d'écrire du code :\n\n1. analyser complètement le repository;\n2. identifier stack frontend/backend;\n3. identifier modèles de données;\n4. comprendre les crawlers/importers existants;\n5. comprendre comment les listings sont normalisés;\n6. comprendre comment les fiches Lou-Ka sont générées;\n7. identifier les données historiques déjà conservées;\n8. identifier les APIs et datasets déjà utilisés;\n9. identifier les composants UI réutilisables;\n10. identifier les jobs/background workers existants;\n11. identifier les systèmes de cache;\n12. identifier les champs déjà disponibles mais non exploités.\n\nNe reconstruis pas ce qui existe déjà.\n\nRéutilise l'architecture existante autant que raisonnablement possible.\n\nAvant l'implémentation, crée un plan technique précis.\n\nEnsuite, implémente réellement les fonctionnalités.\n\n---\n\n# 2. PRINCIPE FONDAMENTAL : DONNÉES > DÉCORATION\n\nChaque nouvelle information doit être classée comme :\n\n* `observed` : directement observée dans une source;\n* `calculated` : calcul déterministe;\n* `estimated` : estimation statistique;\n* `inferred` : inférence;\n* `user_reported` : information provenant d'avis/utilisateurs.\n\nChaque donnée enrichie devrait idéalement conserver :\n\n* source;\n* date de collecte;\n* date de dernière vérification;\n* confidence score;\n* méthode;\n* version de l'algorithme lorsque pertinent.\n\nNe jamais présenter une estimation comme un fait.\n\n---\n\n# 3. COÛT RÉEL DU LOGEMENT\n\nAjouter un module :\n\n## Coût réel mensuel Lou-Ka\n\nCalculer autant que possible :\n\nloyer\n\n* stationnement\n* chauffage\n* électricité\n* eau chaude\n* Internet\n* autres frais obligatoires\n\nmoins les éléments explicitement inclus.\n\nAfficher par exemple :\n\n**Coût réel estimé : 1 742 $ / mois**\n\nPuis :\n\n* loyer affiché;\n* frais supplémentaires connus;\n* frais estimés;\n* services inclus;\n* coût annuel;\n* coût par pi².\n\nSi certaines informations sont inconnues, le dire explicitement.\n\nNe jamais inventer un montant.\n\n---\n\n# 4. PRIX PAR PI²\n\nCalculer :\n\n`rent_per_sqft = monthly_rent / area_sqft`\n\nComparer avec :\n\n* même type de logement;\n* même secteur;\n* rayon géographique pertinent;\n* immeuble si suffisamment de données;\n* comparables Lou-Ka existants.\n\nAfficher :\n\n**2,10 $ / pi² / mois**\n\net éventuellement :\n\n**Plus cher que 73 % des logements comparables**\n\nLe percentile doit être réellement calculé.\n\n---\n\n# 5. HISTORIQUE DES ANNONCES\n\nC'est une fonctionnalité stratégique.\n\nÀ partir des crawls Lou-Ka, conserver des snapshots des listings.\n\nCréer une structure permettant de détecter :\n\n* première apparition;\n* dernière observation;\n* nombre de jours sur le marché;\n* changements de prix;\n* changement de disponibilité;\n* changement de description;\n* changement de superficie;\n* changement des inclusions;\n* changement des photos;\n* disparition;\n* réapparition.\n\nExemple UI :\n\n### Historique Lou-Ka\n\nPremière observation : 4 juin 2026\nEn ligne depuis : 81 jours\nPrix initial : 1 695 $\nPrix actuel : 1 610 $\nVariation : −5,0 %\n2 modifications détectées\n\nAfficher une timeline lorsque suffisamment de données existent.\n\n---\n\n# 6. DÉTECTION DES ANNONCES RECYCLÉES\n\nConstruire une méthode robuste pour reconnaître un même logement lorsqu'il est republié.\n\nUtiliser plusieurs signaux :\n\n* adresse normalisée;\n* numéro d'appartement;\n* gestionnaire;\n* superficie;\n* nombre de chambres;\n* texte;\n* images;\n* perceptual hashes des images;\n* caractéristiques.\n\nCréer un score de similarité.\n\nExemple :\n\n**Annonce probablement déjà observée**\n\nAncienne annonce :\n1 475 $ / mois — février 2025\n\nConfiance : 94 %\n\nNe jamais fusionner automatiquement deux logements avec un niveau de confiance insuffisant.\n\n---\n\n# 7. PASSEPORT DE L'IMMEUBLE\n\nCréer une entité persistante `Building` ou équivalent si elle n'existe pas.\n\nNormaliser les adresses afin de regrouper les appartements appartenant au même bâtiment.\n\nPour chaque immeuble, calculer lorsque possible :\n\n* nombre d'unités Lou-Ka observées;\n* nombre d'annonces historiques;\n* loyer médian;\n* loyer médian par nombre de chambres;\n* prix/pi² médian;\n* évolution des loyers;\n* nombre de nouvelles annonces 30/90/365 jours;\n* fréquence de rotation;\n* gestionnaires observés;\n* distribution des disponibilités;\n* historique des unités.\n\nLa fiche logement doit pouvoir afficher :\n\n### Cet immeuble sur Lou-Ka\n\n32 unités observées\n14 annonces durant les 12 derniers mois\nLoyer médian : 1 485 $\nVariation 12 mois : +6,8 %\n\n---\n\n# 8. SCORE DE ROTATION\n\nCréer un indicateur de rotation des logements.\n\nNe pas inventer un taux de rotation démographique.\n\nIl doit mesurer uniquement ce que Lou-Ka peut réellement observer.\n\nExemples de variables :\n\n* nombre d'unités uniques annoncées;\n* fréquence de réapparition;\n* nombre d'annonces / nombre d'unités connues;\n* historique disponible;\n* période d'observation.\n\nClassifications possibles :\n\n* faible;\n* normale;\n* élevée;\n* très élevée.\n\nToujours permettre d'afficher la méthodologie.\n\n---\n\n# 9. PRESSION SUR LES LOYERS\n\nÀ partir de l'historique Lou-Ka :\n\nmesurer l'évolution des loyers demandés :\n\n* logement;\n* immeuble;\n* rayon;\n* quartier;\n* type de logement.\n\nAfficher éventuellement :\n\n**Nouveaux loyers affichés dans cet immeuble : +11,4 % sur 24 mois**\n\nComparer au secteur lorsque suffisamment de données existent.\n\nNe calculer aucun indicateur avec un échantillon insuffisant.\n\n---\n\n# 10. LOYER PRÉCÉDENT PROBABLE\n\nUtiliser intelligemment :\n\n* Registre des loyers;\n* historique Lou-Ka;\n* même unité;\n* même immeuble;\n* caractéristiques;\n* annonces historiques.\n\nSi le véritable loyer précédent est connu :\n\nl'afficher avec la source.\n\nSinon, éventuellement produire :\n\n**Loyer précédent estimé : 1 300–1 390 $**\n\navec :\n\n* confidence;\n* sources;\n* méthode;\n* mention explicite « estimation ».\n\nCette information est sensible à l'erreur : être conservateur.\n\n---\n\n# 11. ANALYSE DES PHOTOS\n\nCréer une architecture permettant une analyse automatique des photos si le projet possède déjà accès à un modèle multimodal ou à une API appropriée.\n\nDétecter lorsque raisonnablement observable :\n\n* luminosité;\n* fenêtres;\n* balcon;\n* électroménagers;\n* laveuse/sécheuse;\n* état apparent de la cuisine;\n* état apparent de la salle de bain;\n* rénovation apparente;\n* planchers;\n* rangement;\n* climatisation visible.\n\nObjectif important :\n\n## détecter les contradictions\n\nExemple :\n\nAnnonce : « balcon privé »\n\nPhotos : aucun balcon identifiable.\n\nDans ce cas :\n\n**Balcon déclaré par l'annonce — non vérifiable sur les photos**\n\net NON :\n\n**Pas de balcon.**\n\n---\n\n# 12. ENSOLEILLEMENT / LUMIÈRE NATURELLE\n\nSi les données géospatiales nécessaires sont disponibles :\n\ncombiner :\n\n* orientation;\n* étage;\n* bâtiments environnants;\n* distance aux bâtiments;\n* hauteur approximative;\n* exposition.\n\nCréer :\n\n### Potentiel de lumière naturelle\n\n82 / 100\n\nExposition dominante : sud-ouest\nObstruction : faible\nSoleil potentiel : surtout après-midi\n\nNe pas implémenter de pseudo-science.\n\nSi les données ne permettent pas une estimation défendable, ne pas afficher le score.\n\n---\n\n# 13. VIS-À-VIS\n\nÀ partir de la géométrie des bâtiments :\n\ndétecter les bâtiments proches susceptibles de créer :\n\n* obstruction;\n* vis-à-vis;\n* réduction de lumière.\n\nExemple :\n\n**Bâtiment résidentiel à ≈ 16 m côté est**\n\nPrésenter cela comme information géométrique et non comme certitude sur la vue réelle de l'appartement.\n\n---\n\n# 14. BRUIT PRÉDICTIF\n\nAméliorer le KA Score « calme ».\n\nAnalyser si les datasets sont disponibles :\n\n* autoroutes;\n* grandes artères;\n* débit routier;\n* voies ferrées;\n* aéroport;\n* bars;\n* établissements nocturnes;\n* écoles;\n* hôpitaux;\n* casernes;\n* chantiers.\n\nCréer des sous-indicateurs :\n\nTrafic : faible\nTrain : moyen\nVie nocturne : faible\nAéroport : très faible\n\nPuis produire un score global explicable.\n\n---\n\n# 15. SCORE HIVER QUÉBEC\n\nCréer un indicateur réellement adapté au Québec.\n\nVariables potentielles :\n\n* proximité transport;\n* proximité épicerie;\n* proximité pharmacie;\n* marche nécessaire;\n* pente;\n* exposition;\n* accès automobile;\n* services accessibles à pied.\n\nExemple :\n\n### Vie quotidienne en hiver\n\n**Très pratique — 86/100**\n\nÉpicerie : 6 min\nPharmacie : 7 min\nBus : 1 min\n\nLe score doit rester explicable.\n\n---\n\n# 16. ISOCHRONES\n\nAjouter des zones :\n\n* 5 minutes;\n* 10 minutes;\n* 15 minutes.\n\nPour :\n\n* marche;\n* vélo;\n* éventuellement transport.\n\nCalculer les services accessibles :\n\n* épiceries;\n* pharmacies;\n* parcs;\n* écoles;\n* restaurants;\n* cafés;\n* cliniques;\n* transport;\n* autres services utiles.\n\nÉviter de simplement utiliser un rayon circulaire si le système possède un moteur de routage.\n\n---\n\n# 17. DESTINATIONS PERSONNELLES\n\nPréparer l'architecture permettant éventuellement à un utilisateur connecté d'ajouter :\n\n* Travail;\n* Université;\n* École;\n* conjoint/conjointe;\n* famille;\n* destination personnalisée.\n\nAfficher ensuite :\n\nVoiture\nTransport collectif\nVélo\nMarche\n\navec temps estimé.\n\nCette fonctionnalité doit être facultative et respecter la confidentialité.\n\n---\n\n# 18. CONFORT CLIMATIQUE\n\nÀ partir des données disponibles :\n\n* îlot de chaleur;\n* étage;\n* climatisation;\n* végétation;\n* exposition;\n* type de bâtiment;\n* année approximative.\n\nCréer éventuellement :\n\n### Risque de surchauffe estivale\n\nFaible / modéré / élevé\n\navec facteurs explicatifs.\n\nNe pas présenter cela comme une mesure intérieure réelle.\n\n---\n\n# 19. VERDURE\n\nCréer un indicateur différent de « parc à proximité ».\n\nMesurer si possible :\n\n* couverture végétale;\n* arbres;\n* parcs;\n* espaces verts;\n* végétation immédiate.\n\nRayons :\n\n50 m\n100 m\n250 m\n500 m\n\nAfficher :\n\n### Verdure immédiate\n\n71 / 100\n\n---\n\n# 20. CHANTIERS ET DÉVELOPPEMENTS FUTURS\n\nChercher les datasets municipaux disponibles dans l'architecture actuelle.\n\nLorsque possible :\n\n* permis de construction;\n* grands projets;\n* travaux routiers;\n* nouvelles constructions;\n* démolitions;\n* changements majeurs.\n\nAfficher uniquement les données suffisamment géolocalisées.\n\nExemple :\n\n**Projet immobilier important identifié à ≈ 140 m**\n\navec source et date.\n\n---\n\n# 21. INTERNET\n\nLorsque les données sont disponibles :\n\nidentifier :\n\n* fournisseurs;\n* technologies;\n* fibre;\n* câble;\n* vitesse maximale annoncée.\n\nDistinguer :\n\n« Internet inclus dans le bail »\n\nde :\n\n« infrastructures Internet disponibles à l'adresse ».\n\n---\n\n# 22. GESTIONNAIRE / PROPRIÉTAIRE — NOUVELLE SECTION MAJEURE\n\nCréer une vraie fiche de **gestionnaire immobilier**.\n\nExemples :\n\nLogis-Experts\nBoardwalk\nCAPREIT\nCogir\netc.\n\nL'entité doit être persistante.\n\nConserver notamment :\n\n* nom canonique;\n* variantes de nom;\n* site;\n* téléphone public;\n* Google Maps identifiers;\n* bâtiments Lou-Ka associés;\n* listings Lou-Ka;\n* historique;\n* données d'avis.\n\n---\n\n# 23. SERPAPI\n\nNous allons utiliser SerpApi pour rechercher les établissements et récupérer leurs avis Google Maps.\n\nIMPORTANT :\n\nLa clé doit être fournie uniquement via :\n\n`SERPAPI_API_KEY`\n\ndans les variables d'environnement.\n\nNe jamais :\n\n* hardcoder la clé;\n* committer `.env`;\n* exposer la clé au frontend;\n* envoyer la clé au navigateur;\n* écrire la clé dans les logs.\n\nAjouter :\n\n`.env.example`\n\navec :\n\n`SERPAPI_API_KEY=`\n\nL'intégration doit être exclusivement server-side.\n\nDocumentation officielle à utiliser :\n\nSerpApi Google Maps API\nSerpApi Google Maps Reviews API\n\n---\n\n# 24. RÉSOLUTION DU GESTIONNAIRE SUR GOOGLE MAPS\n\nAvant de récupérer les reviews, résoudre correctement l'entreprise.\n\nRecherche à partir de :\n\n* nom;\n* ville;\n* province;\n* adresse publique connue;\n* site Web;\n* téléphone.\n\nNe PAS simplement sélectionner aveuglément le premier résultat Google Maps.\n\nConstruire un `manager_match_confidence`.\n\nComparer :\n\n* nom;\n* domaine;\n* téléphone;\n* ville;\n* adresse;\n* catégorie.\n\nStocker :\n\n* `place_id`;\n* `data_id`;\n* nom Google;\n* adresse;\n* rating;\n* review count;\n* match confidence;\n* date de résolution.\n\nSi confiance insuffisante :\n\nne pas associer automatiquement.\n\n---\n\n# 25. GOOGLE MAPS REVIEWS VIA SERPAPI\n\nUtiliser :\n\n`engine=google_maps_reviews`\n\navec le `data_id` ou `place_id` résolu.\n\nPour une synchronisation complète, récupérer les pages avec :\n\n`next_page_token`\n\njusqu'à :\n\n* épuisement;\n* limite de sécurité;\n* limite configurable.\n\nUtiliser `newestFirst` lorsque pertinent pour récupérer systématiquement les avis.\n\nSupporter :\n\n`hl=fr`\n\ntout en conservant le texte original retourné.\n\nNe pas lancer l'API à chaque affichage de fiche.\n\nCréer un cache/database local.\n\n---\n\n# 26. MODÈLE DE DONNÉES DES REVIEWS\n\nCréer une table appropriée.\n\nExemple conceptuel :\n\n`PropertyManagerReview`\n\navec :\n\n* id;\n* manager_id;\n* external_review_id;\n* source;\n* rating;\n* text;\n* published_at;\n* relative_date_raw;\n* author_name;\n* author_review_count;\n* owner_response;\n* owner_response_date;\n* source_url;\n* fetched_at;\n* content_hash.\n\nDédupliquer.\n\nNe jamais créer deux fois le même review.\n\n---\n\n# 27. SYNCHRONISATION DES REVIEWS\n\nCréer un service du type :\n\n`syncManagerReviews(managerId)`\n\nIl doit :\n\n1. résoudre Google Maps si nécessaire;\n2. récupérer les reviews;\n3. paginer;\n4. normaliser;\n5. dédupliquer;\n6. upsert;\n7. calculer les statistiques;\n8. enregistrer `last_synced_at`.\n\nPrévoir :\n\n* retry;\n* timeout;\n* rate limiting;\n* backoff;\n* erreurs SerpApi;\n* réponse partielle;\n* logs propres.\n\n---\n\n# 28. ANALYSE DES AVIS\n\nCréer une couche analytique.\n\nNE PAS simplement afficher la moyenne Google.\n\nCalculer :\n\n* rating moyen;\n* nombre d'avis;\n* distribution 1–5 étoiles;\n* moyenne récente;\n* évolution temporelle;\n* proportion 1–2 étoiles;\n* proportion 4–5 étoiles.\n\nAnalyser ensuite les thèmes.\n\nExemples :\n\n* entretien;\n* réparations;\n* délais;\n* communication;\n* personnel;\n* propreté;\n* chauffage;\n* eau chaude;\n* insectes;\n* bruit;\n* sécurité;\n* ascenseurs;\n* stationnement;\n* remboursement/dépôt;\n* augmentation de loyer;\n* service client.\n\n---\n\n# 29. REVIEW INTELLIGENCE\n\nCréer un système de classification des avis.\n\nChaque avis peut recevoir :\n\n* topics;\n* sentiment;\n* severity;\n* confidence.\n\nExemple :\n\n```\n{\n \"topics\": [\n \n```\n avec cle pour sperapi : cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33","ts":1787607857418} {"type":"status","state":"running"} {"type":"claude","event":{"type":"system","subtype":"init","cwd":"/Users/simon-pierreboucher/apps/lou-ka","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-fable-5","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","cost","heapdump","init","review","security-review","insights","team-onboarding"],"apiKeySource":"ANTHROPIC_API_KEY","claude_code_version":"2.1.117","output_style":"default","agents":["Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[],"uuid":"45a6616f-afb6-4f5b-9e96-1588e115cd5b","memory_paths":{"auto":"/Users/simon-pierreboucher/.claude/projects/-Users-simon-pierreboucher-apps-lou-ka/memory/"},"fast_mode_state":"off"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c9f794df-662e-42df-8440-414141d82127","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJc4Qa9LCUgsnRNzBAZ","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISlAsKpwEIERgCKkDVpgtXZuO1Cnd1g5LkFlnVcDfKQ0iZ5BAvWi1wTX2AMnrTEQRoQwPgoSQ/SDR3eh6m7PmwqbHahZlsq30C/+cmMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG69rLUBrABAhIMlsgGeYGc+ojkYilFGgwBsQyInD5MyO7cskkiMP0Ecv9OnCf87f+V2eNZd7dfnI30xHtvpuYNhymt+5tnmypuJTXjA7gARBFwov7SeCqZCXSaZ/jC5rsazEFss1RTIOUSkOOrS/Yxx7kGQxQ+10nka2w9k2cofn7MNUmYEAIWruWVYj5scvjK+wUCxxsZsqaeUULxeYu+FG2voRmJcke0HjwMFermgXAeGE1P/oRlJFUr0n5lFSYI23Pm2OGfbtMkHWAR5amKylu0dwyuvZLuead2ljnxfWQc6i6oHeD5OHeH0VvKM4Chu+E1Ja2Ev5wgcsg0lH8iHkoRpFWyVSpj1Ob2uOt4eMsPh2ttdUMCvFu5wjWaApFk3R37h7eZUzWDNRWMs6CLdTOFvUSxsXeQhol6l3cYtbPonW236VdJ8WX0+MbpEnDNIWyJuvYk2iujr1c+nmj4a+g3K8IhL2fAO6qf1UrLKeFtE7pyDouZeIMDOjLHnZrOsC6AtnrlQdq18kkI+sWc2xMndUmX75o7l87TsHRFjAQcCdKAK1Xf6kGcRJ8sDeGMX+buPhP3akyv9YQZy32JHJ+/wPcm2GGn97uGONmHbhZWU5ZtXYJZtEAEdkukp19FtEhM1Or08TnFQ85YBAqI+12pFK2ds0YUDFbpM/pvz1voCUmPQEKCuw2wW73nJYVfLnQPnv9N4WpFmn3bC6Xn2UO8th/xiN0/C3oxfX6drdBNDMOhQuwmgiQC4dheAQghwvv/4x0bmKGzl231Fz8P1lLjSXufJZe4ZvDBuln71M4DnQItHHOrXgHMNCyl3/0578XeCpYmgkO9nuM58RXZ5stRq179DewmSipebeyC8CkIcOi2Wp3DH83Ef+H5Jlq+D24ogXYA51dNcoV4DhMWez+nVADzb/uE1iF9ALBEWfCsPaL0uwfLMYb7F7HK+Vfeqyb4fJSsvlcn3ZXlityG6eaHXs4+O3jBv7uIlHns42BpIM/h1suqLXuymwphn17YNJSi4zKAbRiZE9kMKx4+nK2i/iE2zbWEhAnEnu599qyovV0WLB6grUwT76HjY6Bp3GgFxgqvtW3L64xPNPkvxrFTga6Ieh4t2qOLWxVPnzv4eXFAeLMm3J/4CqPIU3v6e4hstcZ9Dsg+3e1RBMY0iLtJp1NM8UD0jZYMKOL/L7RGvONER6BAhkQEqGt5kAs32LtCdTfQwoHej+Xg8IEV5Y/ZBM8GshfzYQJFdBG74lp9Aod+CS4OQwYoBT/vm4q4QDMKp/nfvvs8OVnPXxa3W4lttI4kgP6xmVreCk0HlEz5ELjdKXQxWMannhXQhQ0cswiG7s6jgqGWI9eSk+GvSuQJNdnq3iUBa8SGrwcGOevCDAgJQfMsa4/rvhbov7ln7JIGK30pbGw27a9HPHuTtMq2eLAZVLCHj830NpZFR0P/z9KGjCQZkE+Y4GfDQeK4lfPxfM9y5VEXXHvW95Jh1z3HQLfFCtNUHr+AHk0YkLyyaM7tv8gcbhTi3apEei2GbHLcxk9RG3SMa7FzNjmv++UgrX10qL19EQTm22TOfVPlkzYcSHLNeMRyQW7BUVORiJeqkftJECGoLw8fOtnkAhHXYFYqqNDnnsfgoERFtY4XOjyGDXYx55STKI3HDbzDixJNoQ5quHoVFRpAQVAoHNYYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":40477,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":40477,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a770fde2-b70a-48c4-b7d7-519cc9393074"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJc4Qa9LCUgsnRNzBAZ","type":"message","role":"assistant","content":[{"type":"text","text":"Mission reçue. Je commence par l'audit complet du repo avant tout code. Je lance une exploration approfondie en parallèle avec un survol rapide de la structure moi-même."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":40477,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":40477,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9e64e177-7ddc-4743-bbf4-5f96c1b45db5"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJc4Qa9LCUgsnRNzBAZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QG6KVuLMs7zcMzNcSABT3r","name":"Bash","input":{"command":"ls -la ~/apps/lou-ka && cat ~/apps/lou-ka/package.json 2>/dev/null; ls ~/apps/lou-ka/backend 2>/dev/null | head -50; ls ~/apps/lou-ka/frontend 2>/dev/null | head -50","description":"Survol de la structure du repo lou-ka"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":40477,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":40477,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"df48bef1-7dae-4eda-b7b8-b988ee9e4fa1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QG6KVuLMs7zcMzNcSABT3r","type":"tool_result","content":"total 264\ndrwxr-xr-x 27 simon-pierreboucher staff 864 Aug 24 16:22 .\ndrwxr-xr-x 20 simon-pierreboucher staff 640 Aug 17 20:27 ..\n-rw-r--r-- 1 simon-pierreboucher staff 20484 Aug 13 04:09 .DS_Store\ndrwxr-xr-x 3 simon-pierreboucher staff 96 Aug 9 08:08 .claude\n-rw-r--r--@ 1 simon-pierreboucher staff 1057 Aug 23 23:11 .env\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 17:11 .git\n-rw-r--r--@ 1 simon-pierreboucher staff 616 Aug 24 16:33 .gitignore\ndrwxr-xr-x 6 simon-pierreboucher staff 192 Aug 8 17:18 .pytest_cache\ndrwxr-xr-x@ 8 simon-pierreboucher staff 256 Aug 17 21:15 .venv\n-rw-r--r-- 1 simon-pierreboucher staff 22852 Aug 24 14:52 README.md\ndrwxr-xr-x@ 3 simon-pierreboucher staff 96 Aug 22 19:37 __pycache__\ndrwxr-xr-x@ 4 simon-pierreboucher staff 128 Aug 22 18:09 actors\ndrwxr-xr-x 12 simon-pierreboucher staff 384 Aug 18 16:34 backup-connectors-20260818\ndrwxr-xr-x 23 simon-pierreboucher staff 736 Aug 24 17:36 data\ndrwxr-xr-x 9 simon-pierreboucher staff 288 Aug 19 21:58 docs\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 16:51 frontend\n-rw-r--r-- 1 simon-pierreboucher staff 45478 Aug 6 11:08 gestion-immobiliere-quebec.md\ndrwxr-xr-x 41 simon-pierreboucher staff 1312 Aug 24 17:09 louka\ndrwxr-xr-x@ 6 simon-pierreboucher staff 192 Aug 18 18:12 node_modules\n-rw-r--r--@ 1 simon-pierreboucher staff 1746 Aug 18 18:12 package-lock.json\n-rw-r--r--@ 1 simon-pierreboucher staff 544 Aug 18 18:12 package.json\n-rw-r--r-- 1 simon-pierreboucher staff 5662 Aug 8 20:38 reponse.txt\ndrwxr-xr-x 13 simon-pierreboucher staff 416 Aug 18 18:14 reports\n-rw-r--r-- 1 simon-pierreboucher staff 226 Aug 17 21:26 requirements.txt\n-rw-r--r--@ 1 simon-pierreboucher staff 5995 Aug 24 16:22 run.py\ndrwxr-xr-x 12 simon-pierreboucher staff 384 Aug 18 20:53 scripts\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 21 17:19 tests\n{\n \"name\": \"lou-ka\",\n \"version\": \"1.0.0\",\n \"description\": \"**[www.lou-ka.com](https://www.lou-ka.com)** — un service **[Groupe Ka](https://www.groupe-ka.com)**\",\n \"main\": \"index.js\",\n \"directories\": {\n \"doc\": \"docs\",\n \"test\": \"tests\"\n },\n \"scripts\": {\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"gitsrv:srv/git/lou-ka.git\"\n },\n \"keywords\": [],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"type\": \"commonjs\",\n \"devDependencies\": {\n \"playwright\": \"^1.62.1\"\n }\n}\ndist\nindex.html\nnode_modules\npackage-lock.json\npackage.json\npublic\nscripts\nsrc\ntsconfig.json\ntsconfig.tsbuildinfo\nvite.config.ts","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c132d9e1-ec1e-4111-9b3f-111013629c1a","timestamp":"2026-08-24T21:44:29.904Z","tool_use_result":{"stdout":"total 264\ndrwxr-xr-x 27 simon-pierreboucher staff 864 Aug 24 16:22 .\ndrwxr-xr-x 20 simon-pierreboucher staff 640 Aug 17 20:27 ..\n-rw-r--r-- 1 simon-pierreboucher staff 20484 Aug 13 04:09 .DS_Store\ndrwxr-xr-x 3 simon-pierreboucher staff 96 Aug 9 08:08 .claude\n-rw-r--r--@ 1 simon-pierreboucher staff 1057 Aug 23 23:11 .env\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 17:11 .git\n-rw-r--r--@ 1 simon-pierreboucher staff 616 Aug 24 16:33 .gitignore\ndrwxr-xr-x 6 simon-pierreboucher staff 192 Aug 8 17:18 .pytest_cache\ndrwxr-xr-x@ 8 simon-pierreboucher staff 256 Aug 17 21:15 .venv\n-rw-r--r-- 1 simon-pierreboucher staff 22852 Aug 24 14:52 README.md\ndrwxr-xr-x@ 3 simon-pierreboucher staff 96 Aug 22 19:37 __pycache__\ndrwxr-xr-x@ 4 simon-pierreboucher staff 128 Aug 22 18:09 actors\ndrwxr-xr-x 12 simon-pierreboucher staff 384 Aug 18 16:34 backup-connectors-20260818\ndrwxr-xr-x 23 simon-pierreboucher staff 736 Aug 24 17:36 data\ndrwxr-xr-x 9 simon-pierreboucher staff 288 Aug 19 21:58 docs\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 16:51 frontend\n-rw-r--r-- 1 simon-pierreboucher staff 45478 Aug 6 11:08 gestion-immobiliere-quebec.md\ndrwxr-xr-x 41 simon-pierreboucher staff 1312 Aug 24 17:09 louka\ndrwxr-xr-x@ 6 simon-pierreboucher staff 192 Aug 18 18:12 node_modules\n-rw-r--r--@ 1 simon-pierreboucher staff 1746 Aug 18 18:12 package-lock.json\n-rw-r--r--@ 1 simon-pierreboucher staff 544 Aug 18 18:12 package.json\n-rw-r--r-- 1 simon-pierreboucher staff 5662 Aug 8 20:38 reponse.txt\ndrwxr-xr-x 13 simon-pierreboucher staff 416 Aug 18 18:14 reports\n-rw-r--r-- 1 simon-pierreboucher staff 226 Aug 17 21:26 requirements.txt\n-rw-r--r--@ 1 simon-pierreboucher staff 5995 Aug 24 16:22 run.py\ndrwxr-xr-x 12 simon-pierreboucher staff 384 Aug 18 20:53 scripts\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 21 17:19 tests\n{\n \"name\": \"lou-ka\",\n \"version\": \"1.0.0\",\n \"description\": \"**[www.lou-ka.com](https://www.lou-ka.com)** — un service **[Groupe Ka](https://www.groupe-ka.com)**\",\n \"main\": \"index.js\",\n \"directories\": {\n \"doc\": \"docs\",\n \"test\": \"tests\"\n },\n \"scripts\": {\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"gitsrv:srv/git/lou-ka.git\"\n },\n \"keywords\": [],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"type\": \"commonjs\",\n \"devDependencies\": {\n \"playwright\": \"^1.62.1\"\n }\n}\ndist\nindex.html\nnode_modules\npackage-lock.json\npackage.json\npublic\nscripts\nsrc\ntsconfig.json\ntsconfig.tsbuildinfo\nvite.config.ts","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJc4Qa9LCUgsnRNzBAZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","name":"Agent","input":{"description":"Audit complet du repo lou-ka","subagent_type":"Explore","prompt":"Fais un audit TRÈS APPROFONDI (very thorough) du repo /Users/simon-pierreboucher/apps/lou-ka (agrégateur québécois de logements locatifs, en production). C'est de la recherche seulement — n'écris aucun code.\n\nJe prépare un upgrade majeur des fiches logement (historique d'annonces/snapshots, entité Immeuble, entité Gestionnaire + avis Google via SerpApi, coût réel mensuel, prix/pi², détection d'annonces recyclées, scores explicables). J'ai besoin de comprendre l'existant pour NE PAS reconstruire ce qui existe.\n\nRapporte précisément (avec chemins de fichiers + numéros de lignes clés) :\n\n1. **Stack** : frontend (framework, build), backend (langage, framework, serveur), base de données (SQLite/Postgres? ORM ou SQL brut?), comment l'app est servie (PM2? port?).\n2. **Modèle de données** : schéma complet des tables (listings, prix, photos, etc.), où le schéma est défini/migré. Y a-t-il déjà des tables building/immeuble, gestionnaire/landlord, historique de prix, snapshots?\n3. **Crawlers/importers** : liste des connecteurs/sources, comment ils tournent (cron? PM2?), pipeline de normalisation (adresse, prix, superficie), déduplication existante, fichier quality.py et relevance_reason().\n4. **Génération des fiches** : comment une fiche logement est rendue (SSR? API + JS? templates?), structure de la page détail, composants UI réutilisables (cartes, KPI, tableaux, pastilles — il y a eu une passe design « éditorial sharp »), le PDF (fiche PDF récente).\n5. **Enrichissements existants** : où et comment sont calculés/stockés : analyse juste valeur, comparables, distribution loyers, Registre des loyers, historique TAL, inondation, qualité de l'air, commerces, transport, essence, KA Scores, démographie, criminalité, îlots de chaleur. Quelles APIs/datasets externes sont utilisés (clés dans .env?).\n6. **Jobs/workers** : jobs de fond, crons, files d'attente, systèmes de cache.\n7. **Champs disponibles mais non exploités** : colonnes/données collectées mais pas affichées.\n8. **Géospatial** : y a-t-il géocodage, lat/lon, PostGIS ou calculs haversine, moteur de routage/isochrones?\n9. **Photos** : stockage des photos (URLs? téléchargées?), hash d'images existant?\n10. **Adresses** : normalisation d'adresses existante (le module TAL croise déjà par adresse — comment?).\n\nStructure ton rapport par sections numérotées ci-dessus. Sois exhaustif mais factuel. Mentionne la taille de la base de données et le volume de données (nb listings, historique conservé ou non)."},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":40477,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":40477,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"05256dbc-7371-45c1-b008-62c2379c73e8"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Audit complet du repo lou-ka","task_type":"local_agent","prompt":"Fais un audit TRÈS APPROFONDI (very thorough) du repo /Users/simon-pierreboucher/apps/lou-ka (agrégateur québécois de logements locatifs, en production). C'est de la recherche seulement — n'écris aucun code.\n\nJe prépare un upgrade majeur des fiches logement (historique d'annonces/snapshots, entité Immeuble, entité Gestionnaire + avis Google via SerpApi, coût réel mensuel, prix/pi², détection d'annonces recyclées, scores explicables). J'ai besoin de comprendre l'existant pour NE PAS reconstruire ce qui existe.\n\nRapporte précisément (avec chemins de fichiers + numéros de lignes clés) :\n\n1. **Stack** : frontend (framework, build), backend (langage, framework, serveur), base de données (SQLite/Postgres? ORM ou SQL brut?), comment l'app est servie (PM2? port?).\n2. **Modèle de données** : schéma complet des tables (listings, prix, photos, etc.), où le schéma est défini/migré. Y a-t-il déjà des tables building/immeuble, gestionnaire/landlord, historique de prix, snapshots?\n3. **Crawlers/importers** : liste des connecteurs/sources, comment ils tournent (cron? PM2?), pipeline de normalisation (adresse, prix, superficie), déduplication existante, fichier quality.py et relevance_reason().\n4. **Génération des fiches** : comment une fiche logement est rendue (SSR? API + JS? templates?), structure de la page détail, composants UI réutilisables (cartes, KPI, tableaux, pastilles — il y a eu une passe design « éditorial sharp »), le PDF (fiche PDF récente).\n5. **Enrichissements existants** : où et comment sont calculés/stockés : analyse juste valeur, comparables, distribution loyers, Registre des loyers, historique TAL, inondation, qualité de l'air, commerces, transport, essence, KA Scores, démographie, criminalité, îlots de chaleur. Quelles APIs/datasets externes sont utilisés (clés dans .env?).\n6. **Jobs/workers** : jobs de fond, crons, files d'attente, systèmes de cache.\n7. **Champs disponibles mais non exploités** : colonnes/données collectées mais pas affichées.\n8. **Géospatial** : y a-t-il géocodage, lat/lon, PostGIS ou calculs haversine, moteur de routage/isochrones?\n9. **Photos** : stockage des photos (URLs? téléchargées?), hash d'images existant?\n10. **Adresses** : normalisation d'adresses existante (le module TAL croise déjà par adresse — comment?).\n\nStructure ton rapport par sections numérotées ci-dessus. Sois exhaustif mais factuel. Mentionne la taille de la base de données et le volume de données (nb listings, historique conservé ou non).","uuid":"7d56cf29-c590-49c1-abf4-a0d4e026e61b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Fais un audit TRÈS APPROFONDI (very thorough) du repo /Users/simon-pierreboucher/apps/lou-ka (agrégateur québécois de logements locatifs, en production). C'est de la recherche seulement — n'écris aucun code.\n\nJe prépare un upgrade majeur des fiches logement (historique d'annonces/snapshots, entité Immeuble, entité Gestionnaire + avis Google via SerpApi, coût réel mensuel, prix/pi², détection d'annonces recyclées, scores explicables). J'ai besoin de comprendre l'existant pour NE PAS reconstruire ce qui existe.\n\nRapporte précisément (avec chemins de fichiers + numéros de lignes clés) :\n\n1. **Stack** : frontend (framework, build), backend (langage, framework, serveur), base de données (SQLite/Postgres? ORM ou SQL brut?), comment l'app est servie (PM2? port?).\n2. **Modèle de données** : schéma complet des tables (listings, prix, photos, etc.), où le schéma est défini/migré. Y a-t-il déjà des tables building/immeuble, gestionnaire/landlord, historique de prix, snapshots?\n3. **Crawlers/importers** : liste des connecteurs/sources, comment ils tournent (cron? PM2?), pipeline de normalisation (adresse, prix, superficie), déduplication existante, fichier quality.py et relevance_reason().\n4. **Génération des fiches** : comment une fiche logement est rendue (SSR? API + JS? templates?), structure de la page détail, composants UI réutilisables (cartes, KPI, tableaux, pastilles — il y a eu une passe design « éditorial sharp »), le PDF (fiche PDF récente).\n5. **Enrichissements existants** : où et comment sont calculés/stockés : analyse juste valeur, comparables, distribution loyers, Registre des loyers, historique TAL, inondation, qualité de l'air, commerces, transport, essence, KA Scores, démographie, criminalité, îlots de chaleur. Quelles APIs/datasets externes sont utilisés (clés dans .env?).\n6. **Jobs/workers** : jobs de fond, crons, files d'attente, systèmes de cache.\n7. **Champs disponibles mais non exploités** : colonnes/données collectées mais pas affichées.\n8. **Géospatial** : y a-t-il géocodage, lat/lon, PostGIS ou calculs haversine, moteur de routage/isochrones?\n9. **Photos** : stockage des photos (URLs? téléchargées?), hash d'images existant?\n10. **Adresses** : normalisation d'adresses existante (le module TAL croise déjà par adresse — comment?).\n\nStructure ton rapport par sections numérotées ci-dessus. Sois exhaustif mais factuel. Mentionne la taille de la base de données et le volume de données (nb listings, historique conservé ou non)."}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e3854281-5f20-4ccd-ac39-c26eaca12e6f","timestamp":"2026-08-24T21:44:45.295Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running find . -type f -name \"*.json\" | head -20","usage":{"total_tokens":8887,"tool_uses":1,"duration_ms":1528},"last_tool_name":"Bash","uuid":"8ef59ca1-ca8f-4adf-8343-96db25ebd5ff","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJe6LDUfC9WaCs47Jn5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CMRX9uNkc2t6DH7b4pg94D","name":"Bash","input":{"command":"find . -type f -name \"*.json\" | head -20"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":8882,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":8882,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"995e233b-795b-458a-8311-1fe9fd4b96a2"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CMRX9uNkc2t6DH7b4pg94D","type":"tool_result","content":"./frontend/node_modules/jsesc/package.json\n./frontend/node_modules/@types/babel__template/package.json\n./frontend/node_modules/@types/react-dom/package.json\n./frontend/node_modules/@types/babel__generator/package.json\n./frontend/node_modules/@types/babel__traverse/package.json\n./frontend/node_modules/@types/prop-types/package.json\n./frontend/node_modules/@types/estree/package.json\n./frontend/node_modules/@types/babel__core/package.json\n./frontend/node_modules/@types/react/package.json\n./frontend/node_modules/browserslist/package.json\n./frontend/node_modules/csstype/package.json\n./frontend/node_modules/@rolldown/pluginutils/package.json\n./frontend/node_modules/loose-envify/package.json\n./frontend/node_modules/ms/package.json\n./frontend/node_modules/node-releases/package.json\n./frontend/node_modules/node-releases/data/processed/envs.json\n./frontend/node_modules/node-releases/data/release-schedule/release-schedule.json\n./frontend/node_modules/lru-cache/package.json\n./frontend/node_modules/escalade/package.json\n./frontend/node_modules/nanoid/async/package.json","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c71660c4-7695-4035-98f4-b7f0d347b934","timestamp":"2026-08-24T21:44:46.922Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka","usage":{"total_tokens":10235,"tool_uses":2,"duration_ms":2935},"last_tool_name":"Bash","uuid":"56b59d52-8a6a-4489-9489-b1e9252dd035","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeDBE4YLMesx8JeoFz","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016ytRJVHi5jGzTYwEsk8RYP","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1342,"cache_read_input_tokens":8882,"cache_creation":{"ephemeral_5m_input_tokens":1342,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"226277be-1fb3-4c6e-8407-8843c4540b25"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016ytRJVHi5jGzTYwEsk8RYP","type":"tool_result","content":"total 264\ndrwxr-xr-x 27 simon-pierreboucher staff 864 Aug 24 16:22 .\ndrwxr-xr-x 20 simon-pierreboucher staff 640 Aug 17 20:27 ..\n-rw-r--r-- 1 simon-pierreboucher staff 20484 Aug 13 04:09 .DS_Store\ndrwxr-xr-x 3 simon-pierreboucher staff 96 Aug 9 08:08 .claude\n-rw-r--r--@ 1 simon-pierreboucher staff 1057 Aug 23 23:11 .env\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 17:11 .git\n-rw-r--r--@ 1 simon-pierreboucher staff 616 Aug 24 16:33 .gitignore\ndrwxr-xr-x 6 simon-pierreboucher staff 192 Aug 8 17:18 .pytest_cache\ndrwxr-xr-x@ 8 simon-pierreboucher staff 256 Aug 17 21:15 .venv\n-rw-r--r-- 1 simon-pierreboucher staff 22852 Aug 24 14:52 README.md\ndrwxr-xr-x@ 3 simon-pierreboucher staff 96 Aug 22 19:37 __pycache__\ndrwxr-xr-x@ 4 simon-pierreboucher staff 128 Aug 22 18:09 actors\ndrwxr-xr-x 12 simon-pierreboucher staff 384 Aug 18 16:34 backup-connectors-20260818\ndrwxr-xr-x 23 simon-pierreboucher staff 736 Aug 24 17:36 data\ndrwxr-xr-x 9 simon-pierreboucher staff 288 Aug 19 21:58 docs\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 16:51 frontend\n-rw-r--r-- 1 simon-pierreboucher staff 45478 Aug 6 11:08 gestion-immobiliere-quebec.md\ndrwxr-xr-x 41 simon-pierreboucher staff 1312 Aug 24 17:09 louka\ndrwxr-xr-x@ 6 simon-pierreboucher staff 192 Aug 18 18:12 node_modules\n-rw-r--r--@ 1 simon-pierreboucher staff 1746 Aug 18 18:12 package-lock.json\n-rw-r--r--@ 1 simon-pierreboucher staff 544 Aug 18 18:12 package.json\n-rw-r--r-- 1 simon-pierreboucher staff 5662 Aug 8 20:38 reponse.txt\ndrwxr-xr-x 13 simon-pierreboucher staff 416 Aug 18 18:14 reports\n-rw-r--r-- 1 simon-pierreboucher staff 226 Aug 17 21:26 requirements.txt\n-rw-r--r--@ 1 simon-pierreboucher staff 5995 Aug 24 16:22 run.py\ndrwxr-xr-x 12 simon-pierreboucher staff 384 Aug 18 20:53 scripts\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 21 17:19 tests","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"197c22e9-f24b-4ae7-b068-aaafae0bf060","timestamp":"2026-08-24T21:44:48.261Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading package.json","usage":{"total_tokens":11284,"tool_uses":3,"duration_ms":4307},"last_tool_name":"Read","uuid":"ae5077b4-875e-469e-b948-3d8443f21818","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeK1DDxbb7QCmdaPxH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01GWaNWa6g9wDVHw3SmGRWLb","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/package.json"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1044,"cache_read_input_tokens":10224,"cache_creation":{"ephemeral_5m_input_tokens":1044,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"beedae0d-cf95-4767-aced-5770f0bbdb7f"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01GWaNWa6g9wDVHw3SmGRWLb","type":"tool_result","content":"1\t{\n2\t \"name\": \"lou-ka\",\n3\t \"version\": \"1.0.0\",\n4\t \"description\": \"**[www.lou-ka.com](https://www.lou-ka.com)** — un service **[Groupe Ka](https://www.groupe-ka.com)**\",\n5\t \"main\": \"index.js\",\n6\t \"directories\": {\n7\t \"doc\": \"docs\",\n8\t \"test\": \"tests\"\n9\t },\n10\t \"scripts\": {\n11\t \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n12\t },\n13\t \"repository\": {\n14\t \"type\": \"git\",\n15\t \"url\": \"gitsrv:srv/git/lou-ka.git\"\n16\t },\n17\t \"keywords\": [],\n18\t \"author\": \"\",\n19\t \"license\": \"ISC\",\n20\t \"type\": \"commonjs\",\n21\t \"devDependencies\": {\n22\t \"playwright\": \"^1.62.1\"\n23\t }\n24\t}\n25\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"662f43d8-17f3-40b6-869f-e8b18d554510","timestamp":"2026-08-24T21:44:49.611Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading requirements.txt","usage":{"total_tokens":11286,"tool_uses":4,"duration_ms":4669},"last_tool_name":"Read","uuid":"4a8d2eb7-e45e-4988-b5dd-99e9726225b0","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeK1DDxbb7QCmdaPxH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01L5BxJvDCmpo24qsoEDX8Uf","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/requirements.txt"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1044,"cache_read_input_tokens":10224,"cache_creation":{"ephemeral_5m_input_tokens":1044,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3a7f0790-9d9d-4ba8-9c26-373fd0328e4f"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01L5BxJvDCmpo24qsoEDX8Uf","type":"tool_result","content":"1\t# Lou-Ka — dépendances backend\n2\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n3\tfastapi>=0.110\n4\tuvicorn>=0.29\n5\trequests>=2.31\n6\tbeautifulsoup4>=4.12\n7\treportlab>=4.0\n8\tpillow>=10.0\n9\tqrcode>=7.4\n10\tpython-multipart>=0.0.9\n11\tfpdf2\n12\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f16e6f4d-52a9-4ea0-bfdc-40aebff6d17c","timestamp":"2026-08-24T21:44:49.970Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading .env","usage":{"total_tokens":11288,"tool_uses":5,"duration_ms":4870},"last_tool_name":"Read","uuid":"b4ccf262-ae16-48ae-aaf5-9d4ae15b11cc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeK1DDxbb7QCmdaPxH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Q7vwRtkgVEm67BFycSMUVx","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/.env"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1044,"cache_read_input_tokens":10224,"cache_creation":{"ephemeral_5m_input_tokens":1044,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2f5e48c5-f360-4fc0-91da-8c1ddac72216"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Q7vwRtkgVEm67BFycSMUVx","type":"tool_result","content":"1\t# Lou-Ka — variables d'environnement (NE PAS COMMITTER)\n2\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n3\tFIRECRAWL_API_KEY=fc-8cc44cb683044aee82e8cbad6505a01a\n4\tSCRAPFLY_KEY=scp-live-4678865fffba41809895d31175deccc0\n5\tSESSION_SECRET=2Qqdikx055p3WeWPzqWy-5_JcPZ5DVAMVT3ZUHHuN63OOkY9JAxKzgGvjOtsFg3z\n6\tLOUKA_BASE_URL=https://www.lou-ka.com\n7\tGOOGLE_CLIENT_ID=380896406983-nfgksio5h3galufqetrk60slffte733d.apps.googleusercontent.com\n8\tGOOGLE_CLIENT_SECRET=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\n9\tKA_SSO_SECRET=f17ecfd8bc06fb3297189e607b6e1d29aecf3f35bf513bfbad0620ed3db653c5\n10\tKA_HUB_URL=https://www.groupe-ka.com\n11\t\n12\t# Apify (acteur maison ka-fb-marketplace) + proxy résidentiel Oxylabs\n13\tAPIFY_TOKEN=apify_api_ptXnhbXohJQn4ATXKMkrXZbP68DUtl2vqgDS\n14\tOXYLABS_PROXY_USER=customer-groupeka_l5ESf\n15\tOXYLABS_PROXY_PASS=xj~zO7+3Hdq_l_\n16\t\n17\t# Bright Data Web Unlocker — acteur ka-apartments-com (apartments.com/Akamai)\n18\tBRIGHTDATA_API_KEY=7396361a-3dff-47f6-b49f-bd8546871215\n19\tBRIGHTDATA_ZONE=web_unlocker1\n20\tHQ_CAPTCHA_KEY=f0e9a45b0c39a8c38e76e0a7b3db765e\n21\tHQ_CAPTCHA_PROVIDER=2captcha\n22\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b43ded9f-8b89-44e3-9027-29bf9406056d","timestamp":"2026-08-24T21:44:50.170Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading run.py","usage":{"total_tokens":11290,"tool_uses":6,"duration_ms":4885},"last_tool_name":"Read","uuid":"66298be6-07ba-48bb-baa9-6580a92d0800","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeK1DDxbb7QCmdaPxH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SEjwgjhLLuj6dggHFdg2Tt","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/run.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1044,"cache_read_input_tokens":10224,"cache_creation":{"ephemeral_5m_input_tokens":1044,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cae0d2dd-948c-4ff4-9bfe-edb954fef4c5"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SEjwgjhLLuj6dggHFdg2Tt","type":"tool_result","content":"1\t#!/usr/bin/env python3\n2\t# -----------------------------------------------------------------------------\n3\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n4\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n5\t# run.py : point d'entrée — `sync`, `watch`, `serve`\n6\t# -----------------------------------------------------------------------------\n7\t\"\"\"Utilisation :\n8\t python run.py sync [source ...] # synchronise les annonces\n9\t python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n10\t python run.py sync-ct [source ...] # synchronise la section court terme\n11\t python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n12\t python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n13\t python run.py record # enregistre les fixtures de test d'une source\n14\t python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n15\t python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n16\t python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n17\t python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n18\t\"\"\"\n19\tfrom __future__ import annotations\n20\t\n21\timport os\n22\timport sys\n23\tfrom pathlib import Path\n24\t\n25\t# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n26\t_env = Path(__file__).parent / \".env\"\n27\tif _env.exists():\n28\t for line in _env.read_text().splitlines():\n29\t line = line.strip()\n30\t if line and not line.startswith(\"#\") and \"=\" in line:\n31\t k, _, v = line.partition(\"=\")\n32\t os.environ.setdefault(k.strip(), v.strip())\n33\t\n34\t\n35\tdef main() -> None:\n36\t cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n37\t if cmd == \"sync\":\n38\t from louka import ingest\n39\t ingest.run(sys.argv[2:] or None)\n40\t elif cmd == \"watch\":\n41\t from louka import ingest\n42\t minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n43\t ingest.watch(minutes * 60)\n44\t elif cmd == \"sync-ct\":\n45\t from louka.shortterm import ingest as ct_ingest\n46\t ct_ingest.run(sys.argv[2:] or None)\n47\t elif cmd == \"watch-ct\":\n48\t from louka.shortterm import ingest as ct_ingest\n49\t minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n50\t ct_ingest.watch(minutes * 60)\n51\t elif cmd == \"geocode1\":\n52\t from louka import geocode\n53\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n54\t geocode.run(limit)\n55\t elif cmd == \"geocode\":\n56\t # lot Adresses Québec (rapide) ; « geocode1 » = ancien mode 1-par-1\n57\t from louka import geocode\n58\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n59\t geocode.run_batch(limit)\n60\t elif cmd == \"quality\":\n61\t # (re)calcule le score de complétude + publication/quarantaine\n62\t from louka import quality\n63\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n64\t quality.backfill(limit)\n65\t elif cmd == \"imgaudit\":\n66\t # contrôle qualité des images (liens morts, minuscules, placeholders,\n67\t # doublons) — incrémental : python run.py imgaudit [n_annonces] [source]\n68\t from louka import imgcheck\n69\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n70\t src = sys.argv[3] if len(sys.argv) > 3 else None\n71\t imgcheck.run(limit, source=src)\n72\t elif cmd == \"fairvalue\":\n73\t # (re)calcule la juste valeur locative de toutes les annonces publiées\n74\t from louka import fairvalue\n75\t fairvalue.compute_all()\n76\t elif cmd == \"poi\":\n77\t from louka import poi\n78\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n79\t poi.run(limit)\n80\t elif cmd == \"quartier\":\n81\t from louka import quartier\n82\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n83\t quartier.enrich(limit)\n84\t elif cmd == \"env\":\n85\t # données d'environnement OSM par tuile (routes, rails, cyclable,\n86\t # bars, POI complets) — cache ~3 mois, socle des KA Scores\n87\t from louka import environment\n88\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n89\t print(environment.run(limit))\n90\t elif cmd == \"kascores\":\n91\t # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n92\t # incrémental ; « python run.py kascores all » force tout le parc\n93\t from louka import kascores\n94\t force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n95\t print(kascores.run(recompute_all=force))\n96\t elif cmd == \"inondation-build\":\n97\t from louka import inondation\n98\t inondation.build()\n99\t elif cmd == \"air-refresh\":\n100\t from louka import air\n101\t air.refresh()\n102\t elif cmd == \"gaz-refresh\":\n103\t from louka import gaz\n104\t print(f\"[gaz] {gaz.refresh()} stations\")\n105\t elif cmd == \"hydro-precompute\":\n106\t from louka import hydro\n107\t n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n108\t b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n109\t hydro.precompute(limit=n, budget=b)\n110\t elif cmd == \"rdl\":\n111\t from louka import rdl\n112\t rdl.refresh()\n113\t elif cmd == \"tal\":\n114\t # historique TAL (décisions SOQUIJ) par adresse, par lots\n115\t from louka import tal\n116\t n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n117\t b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n118\t tal.precompute(limit=n, budget=b)\n119\t elif cmd == \"record\":\n120\t from louka import fixtures\n121\t from louka.connectors import CONNECTORS\n122\t targets = sys.argv[2:] or sorted(CONNECTORS)\n123\t for sid in targets:\n124\t try:\n125\t print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n126\t except Exception as exc:\n127\t print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n128\t elif cmd == \"serve\":\n129\t import uvicorn\n130\t port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n131\t uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n132\t else:\n133\t print(__doc__)\n134\t sys.exit(1)\n135\t\n136\t\n137\tif __name__ == \"__main__\":\n138\t main()\n139\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"af0bd69b-0669-4664-8dff-4ab2bee5b97a","timestamp":"2026-08-24T21:44:50.184Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/lou…","usage":{"total_tokens":15205,"tool_uses":7,"duration_ms":6360},"last_tool_name":"Bash","uuid":"43edde5d-adae-46c2-9b11-1f6fc5c9427a","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeTRyRpspvKugvYRWM","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01E62ugv9d75MU5s9Zxwg5bo","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/louka"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":3910,"cache_read_input_tokens":11268,"cache_creation":{"ephemeral_5m_input_tokens":3910,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2552cfd6-7c32-4f75-b4ef-0467771d3427"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01E62ugv9d75MU5s9Zxwg5bo","type":"tool_result","content":"total 1296\ndrwxr-xr-x 41 simon-pierreboucher staff 1312 Aug 24 17:09 .\ndrwxr-xr-x 27 simon-pierreboucher staff 864 Aug 24 16:22 ..\n-rw-r--r-- 1 simon-pierreboucher staff 10244 Aug 13 04:05 .DS_Store\n-rw-r--r-- 1 simon-pierreboucher staff 383 Aug 6 11:11 __init__.py\ndrwxr-xr-x@ 37 simon-pierreboucher staff 1184 Aug 24 17:10 __pycache__\n-rw-r--r-- 1 simon-pierreboucher staff 8799 Aug 13 02:50 accounts.py\n-rw-r--r-- 1 simon-pierreboucher staff 8535 Aug 23 22:14 air.py\n-rw-r--r-- 1 simon-pierreboucher staff 17760 Aug 13 01:52 auth.py\n-rw-r--r-- 1 simon-pierreboucher staff 11837 Aug 23 22:35 commerces.py\ndrwxr-xr-x 226 simon-pierreboucher staff 7232 Aug 24 09:37 connectors\n-rw-r--r--@ 1 simon-pierreboucher staff 19822 Aug 21 17:16 db.py\n-rw-r--r-- 1 simon-pierreboucher staff 11150 Aug 18 15:22 dedup.py\n-rw-r--r-- 1 simon-pierreboucher staff 7066 Aug 18 18:28 environment.py\n-rw-r--r-- 1 simon-pierreboucher staff 20723 Aug 18 17:14 fairvalue.py\n-rw-r--r-- 1 simon-pierreboucher staff 7909 Aug 9 01:00 fixtures.py\n-rw-r--r-- 1 simon-pierreboucher staff 5354 Aug 23 22:15 gaz.py\n-rw-r--r-- 1 simon-pierreboucher staff 16524 Aug 18 17:27 geocode.py\n-rw-r--r-- 1 simon-pierreboucher staff 2803 Aug 13 02:49 hubfav.py\n-rw-r--r-- 1 simon-pierreboucher staff 3018 Aug 13 01:55 hubprofile.py\n-rw-r--r-- 1 simon-pierreboucher staff 18642 Aug 24 00:07 hydro.py\n-rw-r--r-- 1 simon-pierreboucher staff 10187 Aug 18 16:57 imgcheck.py\n-rw-r--r--@ 1 simon-pierreboucher staff 7039 Aug 24 16:22 ingest.py\n-rw-r--r-- 1 simon-pierreboucher staff 7134 Aug 23 21:59 inondation.py\n-rw-r--r-- 1 simon-pierreboucher staff 51754 Aug 23 17:19 kapdf.py\n-rw-r--r-- 1 simon-pierreboucher staff 19154 Aug 18 18:42 kascores.py\n-rw-r--r-- 1 simon-pierreboucher staff 10626 Aug 9 01:56 marketstats.py\n-rw-r--r-- 1 simon-pierreboucher staff 27889 Aug 18 17:26 normalize.py\n-rw-r--r--@ 1 simon-pierreboucher staff 51778 Aug 24 17:09 pdfgen.py\n-rw-r--r-- 1 simon-pierreboucher staff 10306 Aug 8 21:10 poi.py\n-rw-r--r-- 1 simon-pierreboucher staff 4654 Aug 13 00:45 profile.py\n-rw-r--r--@ 1 simon-pierreboucher staff 10923 Aug 21 17:18 quality.py\n-rw-r--r--@ 1 simon-pierreboucher staff 9471 Aug 23 22:07 quartier.py\n-rw-r--r-- 1 simon-pierreboucher staff 9919 Aug 23 22:04 rdl.py\n-rw-r--r-- 1 simon-pierreboucher staff 7976 Aug 18 17:26 schema.py\n-rw-r--r--@ 1 simon-pierreboucher staff 41042 Aug 23 09:35 seo.py\ndrwxr-xr-x@ 9 simon-pierreboucher staff 288 Aug 23 09:31 shortterm\n-rw-r--r--@ 1 simon-pierreboucher staff 37086 Aug 24 00:24 statsdash.py\n-rw-r--r-- 1 simon-pierreboucher staff 13646 Aug 24 00:28 statsextra.py\n-rw-r--r--@ 1 simon-pierreboucher staff 19171 Aug 24 16:32 tal.py\n-rw-r--r-- 1 simon-pierreboucher staff 31806 Aug 8 22:25 textmine.py\n-rw-r--r--@ 1 simon-pierreboucher staff 40455 Aug 24 16:22 web.py","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b0a9ae4a-c187-4394-8e98-eb2d64c2b70a","timestamp":"2026-08-24T21:44:51.688Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/schema.py","usage":{"total_tokens":16890,"tool_uses":8,"duration_ms":8162},"last_tool_name":"Read","uuid":"1ab6bc51-6549-4841-ab4c-47093b5555d1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeaCGU82q7MT2muJch","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XZSKrks5oKQDkxwcFs5vZk","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/schema.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1681,"cache_read_input_tokens":15178,"cache_creation":{"ephemeral_5m_input_tokens":1681,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"57c83c63-4220-4b9e-93c6-0e46066ca249"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XZSKrks5oKQDkxwcFs5vZk","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# schema.py : modèle de données standardisé (Listing) + enrichissement central\n5\t# -----------------------------------------------------------------------------\n6\t\"\"\"Schéma standard d'une annonce (Listing) et normalisation des champs.\n7\t\n8\tChaque connecteur, peu importe le site source, doit produire des objets\n9\t`Listing` conformes à ce schéma. La méthode `finalize()` applique ensuite\n10\tla couche de normalisation commune (louka/normalize.py) : dates ISO,\n11\tsuperficie en pi², commodités canoniques, prix… — les connecteurs peuvent\n12\tdonc rester simples et remplir les champs bruts.\n13\t\"\"\"\n14\tfrom __future__ import annotations\n15\t\n16\timport hashlib\n17\timport json\n18\tfrom dataclasses import dataclass, field, asdict\n19\t\n20\tfrom .normalize import ( # ré-exportés pour les connecteurs existants\n21\t bedrooms_from_unit_type,\n22\t clean_address,\n23\t clean_city,\n24\t clean_sector,\n25\t coerce_count,\n26\t extract_details,\n27\t merge_details,\n28\t normalize_unit_type,\n29\t parse_area_sqft,\n30\t parse_availability_date,\n31\t parse_bathrooms,\n32\t parse_bedrooms,\n33\t parse_price,\n34\t price_is_from,\n35\t strip_accents,\n36\t)\n37\t\n38\t__all__ = [\n39\t \"Listing\", \"infer_city\", \"normalize_unit_type\", \"parse_price\",\n40\t \"parse_availability_date\", \"parse_area_sqft\", \"clean_address\",\n41\t \"strip_accents\",\n42\t]\n43\t\n44\t\n45\t@dataclass\n46\tclass Listing:\n47\t \"\"\"Annonce standardisée Lou-Ka.\"\"\"\n48\t\n49\t source: str # id de la source (voir data/sources.json)\n50\t external_id: str # identifiant chez la source\n51\t url: str # page de l'annonce chez la source\n52\t title: str = \"\" # ex. \"555, avenue du Fleuve\"\n53\t address: str = \"\" # adresse civique\n54\t sector: str = \"\" # quartier/arrondissement (ex. Beauport)\n55\t city: str = \"\" # Québec, Lévis, ...\n56\t unit_type: str = \"\" # 1½ … 5½, 6½+, Studio, Loft, Chambre…\n57\t bedrooms: float | None = None # chambres fermées (convention QC : 4½ = 2)\n58\t bathrooms: float | None = None # salles de bain (1.5 = salle d'eau en plus)\n59\t price: float | None = None # loyer mensuel ($ CAD), le plus bas si \"à partir de\"\n60\t price_label: str = \"\" # texte original (ex. \"à partir de 799$\")\n61\t availability: str = \"\" # texte original (ex. \"Libre immédiatement\")\n62\t availability_date: str | None = None # ISO \"2026-07-01\", \"now\", ou None\n63\t area_sqft: float | None = None # superficie en pi²\n64\t pets: str | None = None # \"oui\" | \"non\" | \"conditions\" | None\n65\t furnished: bool | None = None # meublé (None = inconnu)\n66\t description: str = \"\"\n67\t digest: dict | None = None # description structurée (louka/textmine.py)\n68\t amenities: list[str] = field(default_factory=list) # texte source, pour affichage\n69\t details: dict = field(default_factory=dict) # champs structurés (JSON)\n70\t images: list[str] = field(default_factory=list) # URLs absolues\n71\t lat: float | None = None\n72\t lng: float | None = None\n73\t\n74\t @property\n75\t def uid(self) -> str:\n76\t return f\"{self.source}:{self.external_id}\"\n77\t\n78\t def content_hash(self) -> str:\n79\t \"\"\"Hash du contenu pour la détection de changements (pseudo-webhook).\"\"\"\n80\t payload = asdict(self)\n81\t blob = json.dumps(payload, sort_keys=True, ensure_ascii=False)\n82\t return hashlib.sha256(blob.encode(\"utf-8\")).hexdigest()\n83\t\n84\t def finalize(self) -> \"Listing\":\n85\t \"\"\"Applique la normalisation commune. Appelé par le pipeline d'ingestion.\n86\t\n87\t Idempotent ; ne remplace jamais une valeur explicite du connecteur.\n88\t \"\"\"\n89\t import html as _html\n90\t self.title = _html.unescape(self.title).strip()\n91\t self.address = clean_address(_html.unescape(self.address))\n92\t self.sector = clean_sector(self.sector)\n93\t self.city = clean_city(self.city)\n94\t self.unit_type = normalize_unit_type(self.unit_type)\n95\t\n96\t if self.price is None:\n97\t self.price = parse_price(self.price_label)\n98\t if self.availability_date is None:\n99\t self.availability_date = parse_availability_date(self.availability)\n100\t if self.area_sqft is None:\n101\t for texte in (self.description, \" \".join(self.amenities), self.title):\n102\t self.area_sqft = parse_area_sqft(texte)\n103\t if self.area_sqft is not None:\n104\t break\n105\t\n106\t # coordonnées fournies par la source : rejeter tout point hors de la\n107\t # province (lat/lng inversés, 0/0, coquilles) — le géocodeur prendra\n108\t # le relais sur l'adresse plutôt que d'afficher un point au Kazakhstan\n109\t if self.lat is not None and self.lng is not None:\n110\t if not (44.5 <= self.lat <= 63.0 and -80.0 <= self.lng <= -56.0):\n111\t self.lat = self.lng = None\n112\t\n113\t derived = extract_details(self.amenities, self.description, self.title)\n114\t if self.price_label and price_is_from(self.price_label):\n115\t derived[\"price_from\"] = True\n116\t self.details = merge_details(self.details, derived)\n117\t\n118\t # chambres / salles de bain : valeur explicite du connecteur d'abord,\n119\t # puis champs structurés de details, puis type d'unité (n½ -> n-2),\n120\t # puis extraction texte — jamais de valeur inventée (None = inconnu)\n121\t if self.bedrooms is None:\n122\t for k in (\"bedrooms\", \"Chambres\", \"Chambre(s)\"):\n123\t self.bedrooms = coerce_count(self.details.get(k))\n124\t if self.bedrooms is not None:\n125\t break\n126\t if self.bedrooms is None:\n127\t self.bedrooms = bedrooms_from_unit_type(self.unit_type)\n128\t if self.bedrooms is None:\n129\t self.bedrooms = parse_bedrooms(self.title, \" | \".join(self.amenities),\n130\t self.description)\n131\t if self.bathrooms is None:\n132\t for k in (\"bathrooms\", \"Salles de bain\", \"Salle de bain\",\n133\t \"Salle(s) de bain\"):\n134\t self.bathrooms = coerce_count(self.details.get(k))\n135\t if self.bathrooms is not None:\n136\t break\n137\t if self.bathrooms is None:\n138\t self.bathrooms = parse_bathrooms(\" | \".join(self.amenities),\n139\t self.description)\n140\t\n141\t # description structurée (nettoyage + extraction + sections)\n142\t if self.digest is None and self.description:\n143\t try:\n144\t from .textmine import analyser\n145\t self.digest = analyser(self.description, price=self.price,\n146\t sector=self.sector, city=self.city)\n147\t except ImportError:\n148\t pass # module absent : la fiche affichera le texte brut\n149\t\n150\t if self.pets is None:\n151\t self.pets = self.details.get(\"pets\")\n152\t else:\n153\t self.details[\"pets\"] = self.pets\n154\t if self.furnished is None:\n155\t furn = self.details.get(\"furnished\")\n156\t self.furnished = furn if isinstance(furn, bool) else None\n157\t else:\n158\t self.details[\"furnished\"] = self.furnished\n159\t return self\n160\t\n161\t\n162\t# ---------------------------------------------------------------------------\n163\t# Secteur -> ville (agglomération Québec / Lévis)\n164\t# ---------------------------------------------------------------------------\n165\t\n166\t_LEVIS_SECTORS = {\n167\t \"levis\", \"saint-romuald\", \"st-romuald\", \"charny\", \"saint-nicolas\",\n168\t \"st-nicolas\", \"saint-david\", \"st-david\", \"desjardins\", \"saint-redempteur\",\n169\t \"st-redempteur\", \"breakeyville\", \"saint-jean-chrysostome\", \"st-jean-chrysostome\",\n170\t \"pintendre\", \"saint-etienne\", \"vieux-levis\",\n171\t}\n172\t\n173\t\n174\tdef infer_city(sector: str, default: str = \"Québec\") -> str:\n175\t key = strip_accents((sector or \"\").strip().lower())\n176\t if key in _LEVIS_SECTORS or \"levis\" in key:\n177\t return \"Lévis\"\n178\t return default\n179\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a768a8c2-c038-4047-8a4f-2a2313db203b","timestamp":"2026-08-24T21:44:53.469Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/db.py","usage":{"total_tokens":16893,"tool_uses":9,"duration_ms":8180},"last_tool_name":"Read","uuid":"1d112049-d285-4cf6-adc7-0a0ff85ac4eb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJeaCGU82q7MT2muJch","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XreYApGQebU6bJREHCzuif","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","limit":200},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1681,"cache_read_input_tokens":15178,"cache_creation":{"ephemeral_5m_input_tokens":1681,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a511d487-0e1d-47ce-bc87-c018fc1b67a4"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XreYApGQebU6bJREHCzuif","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# db.py : persistance SQLite — upsert avec détection de changements,\n5\t# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n6\t# cache des pages détail, cache de géocodage.\n7\t# -----------------------------------------------------------------------------\n8\tfrom __future__ import annotations\n9\t\n10\timport json\n11\timport sqlite3\n12\timport statistics\n13\timport time\n14\tfrom pathlib import Path\n15\t\n16\tfrom .schema import Listing\n17\t\n18\tDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n19\t\n20\t# Nombre d'exécutions consécutives où une annonce doit être absente de la\n21\t# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\n22\tMISS_GRACE = 2\n23\t\n24\t# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n25\t# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\n26\tDRIFT_RATIO = 0.25\n27\tDRIFT_MIN_BASE = 8\n28\tDRIFT_HISTORY = 5\n29\t\n30\t_SCHEMA = \"\"\"\n31\tCREATE TABLE IF NOT EXISTS listings (\n32\t uid TEXT PRIMARY KEY,\n33\t source TEXT NOT NULL,\n34\t external_id TEXT NOT NULL,\n35\t url TEXT,\n36\t title TEXT,\n37\t address TEXT,\n38\t sector TEXT,\n39\t city TEXT,\n40\t unit_type TEXT,\n41\t bedrooms REAL,\n42\t bathrooms REAL,\n43\t price REAL,\n44\t price_label TEXT,\n45\t availability TEXT,\n46\t availability_date TEXT,\n47\t area_sqft REAL,\n48\t pets TEXT,\n49\t furnished INTEGER,\n50\t description TEXT,\n51\t amenities TEXT, -- JSON (liste de textes source)\n52\t details TEXT, -- JSON (champs structurés : inclusions, parking…)\n53\t images TEXT, -- JSON\n54\t lat REAL,\n55\t lng REAL,\n56\t geocode_failed INTEGER DEFAULT 0,\n57\t content_hash TEXT,\n58\t first_seen REAL,\n59\t last_seen REAL,\n60\t updated_at REAL,\n61\t miss_count INTEGER DEFAULT 0,\n62\t active INTEGER DEFAULT 1\n63\t);\n64\tCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\n65\tCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\n66\tCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\n67\tCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n68\t\n69\tCREATE TABLE IF NOT EXISTS sync_log (\n70\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n71\t source TEXT,\n72\t ts REAL,\n73\t found INTEGER,\n74\t added INTEGER,\n75\t updated INTEGER,\n76\t removed INTEGER,\n77\t ok INTEGER,\n78\t message TEXT,\n79\t stats TEXT -- JSON : taux de champs null, missed, alerte…\n80\t);\n81\t\n82\tCREATE TABLE IF NOT EXISTS detail_cache (\n83\t source TEXT NOT NULL,\n84\t external_id TEXT NOT NULL,\n85\t key TEXT, -- hash du contenu « liste » de l'annonce\n86\t payload TEXT, -- JSON opaque propre au connecteur\n87\t fetched_at REAL,\n88\t PRIMARY KEY (source, external_id)\n89\t);\n90\t\n91\tCREATE TABLE IF NOT EXISTS price_log (\n92\t uid TEXT NOT NULL,\n93\t ts REAL NOT NULL,\n94\t price REAL -- prix observé (NULL = retiré de l'affichage)\n95\t);\n96\tCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n97\t\n98\tCREATE TABLE IF NOT EXISTS poi_cache (\n99\t coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n100\t lat REAL,\n101\t lng REAL,\n102\t pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n103\t ts REAL\n104\t);\n105\t\n106\tCREATE TABLE IF NOT EXISTS geocode_cache (\n107\t address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n108\t lat REAL,\n109\t lng REAL,\n110\t provider TEXT,\n111\t failed INTEGER DEFAULT 0,\n112\t ts REAL\n113\t);\n114\t\n115\tCREATE TABLE IF NOT EXISTS users (\n116\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n117\t google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n118\t ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n119\t email TEXT,\n120\t name TEXT,\n121\t picture TEXT, -- URL de l'avatar Google\n122\t public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n123\t role TEXT, -- « locataire » | « gestionnaire » | NULL\n124\t org_source TEXT, -- source réclamée (gestionnaire)\n125\t created_at REAL,\n126\t last_login REAL\n127\t);\n128\t\n129\tCREATE TABLE IF NOT EXISTS favorites (\n130\t user_id INTEGER NOT NULL,\n131\t uid TEXT NOT NULL, -- listings.uid\n132\t ts REAL,\n133\t PRIMARY KEY (user_id, uid)\n134\t);\n135\t\n136\tCREATE TABLE IF NOT EXISTS image_checks (\n137\t url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n138\t status TEXT, -- ok | dead | small | error\n139\t width INTEGER,\n140\t height INTEGER,\n141\t bytes INTEGER,\n142\t sha1 TEXT, -- hash du contenu (doublons / placeholders)\n143\t checked_at REAL\n144\t);\n145\t\n146\tCREATE TABLE IF NOT EXISTS env_tiles (\n147\t tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n148\t data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n149\t fetched_at REAL\n150\t);\n151\t\n152\tCREATE TABLE IF NOT EXISTS kascores (\n153\t coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n154\t lat REAL, lng REAL,\n155\t walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n156\t transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n157\t bike REAL,\n158\t calme REAL,\n159\t services REAL,\n160\t global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n161\t details TEXT, -- JSON : détail par score (fiche + méthodologie)\n162\t version TEXT, -- version du barème (recalcul si changement)\n163\t computed_at REAL\n164\t);\n165\t\n166\tCREATE TABLE IF NOT EXISTS source_profiles (\n167\t source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n168\t owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n169\t tagline TEXT, -- accroche personnalisée\n170\t description TEXT,\n171\t website TEXT,\n172\t phone TEXT,\n173\t email TEXT,\n174\t logo_url TEXT,\n175\t updated_at REAL\n176\t);\n177\t\"\"\"\n178\t\n179\t# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n180\t_MIGRATIONS = {\n181\t \"listings\": {\n182\t \"availability_date\": \"TEXT\",\n183\t \"area_sqft\": \"REAL\",\n184\t \"pets\": \"TEXT\",\n185\t \"furnished\": \"INTEGER\",\n186\t \"details\": \"TEXT\",\n187\t \"geocode_failed\": \"INTEGER DEFAULT 0\",\n188\t \"miss_count\": \"INTEGER DEFAULT 0\",\n189\t \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n190\t \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n191\t \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n192\t \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n193\t \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n194\t \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n195\t \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n196\t \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n197\t \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n198\t \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n199\t \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n200\t \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"47c2e2ce-db5a-4783-827f-6c04f90aab53","timestamp":"2026-08-24T21:44:53.481Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/db.py","usage":{"total_tokens":22614,"tool_uses":10,"duration_ms":10229},"last_tool_name":"Read","uuid":"81cbf312-87a9-4525-b021-8025e216da49","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJehhDYCfhmimzbXcAB","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014bdQoSXveNKj2EEzP3xZvU","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","offset":200,"limit":300},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":5715,"cache_read_input_tokens":16859,"cache_creation":{"ephemeral_5m_input_tokens":5715,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"76f6d57a-cc75-4602-9c08-d46ae06d9cb0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014bdQoSXveNKj2EEzP3xZvU","type":"tool_result","content":"200\t \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n201\t },\n202\t \"sync_log\": {\n203\t \"stats\": \"TEXT\",\n204\t },\n205\t \"users\": {\n206\t \"ka_id\": \"TEXT\",\n207\t \"public\": \"INTEGER DEFAULT 0\",\n208\t \"role\": \"TEXT\",\n209\t \"org_source\": \"TEXT\",\n210\t \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n211\t \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n212\t \"bio\": \"TEXT\",\n213\t \"city\": \"TEXT\",\n214\t \"phone\": \"TEXT\",\n215\t \"website\": \"TEXT\",\n216\t \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n217\t },\n218\t}\n219\t\n220\t\n221\tdef connect() -> sqlite3.Connection:\n222\t DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n223\t con = sqlite3.connect(DB_PATH)\n224\t con.row_factory = sqlite3.Row\n225\t con.executescript(_SCHEMA)\n226\t for table, cols in _MIGRATIONS.items():\n227\t existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n228\t for col, decl in cols.items():\n229\t if col not in existing:\n230\t con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n231\t # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n232\t # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n233\t con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n234\t con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n235\t \" ON listings(active, published)\")\n236\t con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n237\t \" ON listings(coord_key)\")\n238\t con.commit()\n239\t # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n240\t # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n241\t con.execute(\"PRAGMA journal_mode=WAL\")\n242\t con.execute(\"PRAGMA busy_timeout=30000\")\n243\t return con\n244\t\n245\t\n246\t# ---------------------------------------------------------------------------\n247\t# Synchronisation d'une source\n248\t# ---------------------------------------------------------------------------\n249\t\n250\tdef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n251\t null_price_rate: float) -> str | None:\n252\t \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n253\t\n254\t Retourne un message d'alerte, ou None si tout est normal.\n255\t \"\"\"\n256\t hist = con.execute(\n257\t \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n258\t \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n259\t if len(hist) < 3:\n260\t return None\n261\t med_found = statistics.median(r[\"found\"] for r in hist)\n262\t if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n263\t return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n264\t f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n265\t if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n266\t rates = []\n267\t for r in hist:\n268\t try:\n269\t rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n270\t except (KeyError, ValueError, TypeError):\n271\t continue\n272\t if rates and statistics.median(rates) <= 0.3:\n273\t return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n274\t f\"(habituellement {statistics.median(rates):.0%}) — \"\n275\t \"le format de la source a probablement changé\")\n276\t return None\n277\t\n278\t\n279\tdef sync_source(con: sqlite3.Connection, source: str,\n280\t listings: list[Listing]) -> dict:\n281\t \"\"\"Synchronise les annonces d'une source.\n282\t\n283\t - nouvelle annonce -> insertion\n284\t - annonce modifiée -> mise à jour (comparaison de content_hash)\n285\t - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n286\t exécutions consécutives (délai de grâce)\n287\t - dérive détectée -> alerte consignée, retraits suspendus\n288\t \"\"\"\n289\t now = time.time()\n290\t added = updated = 0\n291\t seen_uids = set()\n292\t\n293\t n = len(listings)\n294\t null_price = sum(1 for l in listings if l.price is None)\n295\t null_addr = sum(1 for l in listings if not l.address)\n296\t null_price_rate = round(null_price / n, 3) if n else 0.0\n297\t\n298\t alert = _drift_alert(con, source, n, null_price_rate)\n299\t\n300\t for lst in listings:\n301\t seen_uids.add(lst.uid)\n302\t h = lst.content_hash()\n303\t row = con.execute(\n304\t \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n305\t (lst.uid,)).fetchone()\n306\t # score de complétude + seuil de publication (quarantaine sous le seuil)\n307\t from .quality import evaluate\n308\t q_score, q_pub, q_reasons = evaluate({\n309\t \"title\": lst.title,\n310\t \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n311\t \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n312\t \"description\": lst.description, \"images\": lst.images,\n313\t \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n314\t \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n315\t \"availability_date\": lst.availability_date,\n316\t \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n317\t \"details\": lst.details, \"url\": lst.url})\n318\t params = dict(\n319\t uid=lst.uid, source=lst.source, external_id=lst.external_id,\n320\t url=lst.url, title=lst.title, address=lst.address,\n321\t sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n322\t bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n323\t price=lst.price, price_label=lst.price_label,\n324\t availability=lst.availability,\n325\t availability_date=lst.availability_date,\n326\t area_sqft=lst.area_sqft, pets=lst.pets,\n327\t furnished=(None if lst.furnished is None else int(lst.furnished)),\n328\t description=lst.description,\n329\t digest=(json.dumps(lst.digest, ensure_ascii=False)\n330\t if getattr(lst, \"digest\", None) else None),\n331\t amenities=json.dumps(lst.amenities, ensure_ascii=False),\n332\t details=json.dumps(lst.details, ensure_ascii=False),\n333\t images=json.dumps(lst.images, ensure_ascii=False),\n334\t lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n335\t completeness=q_score, published=int(q_pub),\n336\t quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n337\t if q_reasons else None),\n338\t )\n339\t if row is None:\n340\t con.execute(\n341\t \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n342\t address, sector, city, unit_type, bedrooms, bathrooms,\n343\t price, price_label,\n344\t availability, availability_date, area_sqft, pets, furnished,\n345\t description, digest, amenities, details, images, lat, lng,\n346\t content_hash, first_seen, last_seen, updated_at,\n347\t miss_count, active, completeness, published,\n348\t quality_reasons)\n349\t VALUES (:uid,:source,:external_id,:url,:title,:address,\n350\t :sector,:city,:unit_type,:bedrooms,:bathrooms,\n351\t :price,:price_label,:availability,\n352\t :availability_date,:area_sqft,:pets,:furnished,\n353\t :description,:digest,:amenities,:details,:images,:lat,:lng,\n354\t :content_hash,:now,:now,:now,0,1,\n355\t :completeness,:published,:quality_reasons)\"\"\", params)\n356\t if lst.price is not None: # prix initial = point de départ de l'historique\n357\t con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n358\t (lst.uid, now, lst.price))\n359\t added += 1\n360\t elif row[\"content_hash\"] != h:\n361\t # COALESCE : ne jamais écraser des coordonnées géocodées par null\n362\t con.execute(\n363\t \"\"\"UPDATE listings SET url=:url, title=:title,\n364\t address=:address, sector=:sector, city=:city,\n365\t unit_type=:unit_type, bedrooms=:bedrooms,\n366\t bathrooms=:bathrooms, price=:price,\n367\t price_label=:price_label, availability=:availability,\n368\t availability_date=:availability_date,\n369\t area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n370\t description=:description, digest=:digest,\n371\t amenities=:amenities, details=:details, images=:images,\n372\t lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n373\t content_hash=:content_hash, last_seen=:now,\n374\t updated_at=:now, miss_count=0, active=1,\n375\t completeness=:completeness, published=:published,\n376\t quality_reasons=:quality_reasons\n377\t WHERE uid=:uid\"\"\", params)\n378\t if (row[\"images\"] or \"[]\") != params[\"images\"]:\n379\t # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n380\t con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n381\t \" WHERE uid=?\", (lst.uid,))\n382\t if lst.price != row[\"price\"]: # changement de prix -> historique\n383\t con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n384\t (lst.uid, now, lst.price))\n385\t updated += 1\n386\t else:\n387\t con.execute(\n388\t \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n389\t (now, lst.uid))\n390\t\n391\t # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n392\t # puis désactivation. Suspendu si une dérive est détectée.\n393\t removed = missed = 0\n394\t if not alert:\n395\t for r in con.execute(\n396\t \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n397\t (source,)).fetchall():\n398\t if r[\"uid\"] in seen_uids:\n399\t continue\n400\t missed += 1\n401\t if r[\"miss_count\"] + 1 >= MISS_GRACE:\n402\t con.execute(\n403\t \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n404\t \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n405\t removed += 1\n406\t else:\n407\t con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n408\t (r[\"uid\"],))\n409\t\n410\t stats = {\n411\t \"null_price_rate\": null_price_rate,\n412\t \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n413\t \"missed\": missed,\n414\t }\n415\t if alert:\n416\t stats[\"alert\"] = alert\n417\t con.execute(\n418\t \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n419\t \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n420\t (source, now, n, added, updated, removed, alert or \"ok\",\n421\t json.dumps(stats, ensure_ascii=False)))\n422\t con.commit()\n423\t out = {\"source\": source, \"found\": n, \"added\": added,\n424\t \"updated\": updated, \"removed\": removed}\n425\t if alert:\n426\t out[\"alert\"] = alert\n427\t return out\n428\t\n429\t\n430\tdef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n431\t con.execute(\n432\t \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n433\t \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n434\t con.commit()\n435\t\n436\t\n437\t# ---------------------------------------------------------------------------\n438\t# Cache des pages détail (« détail si nouveau/modifié »)\n439\t# ---------------------------------------------------------------------------\n440\t\n441\tdef get_cached_detail(con: sqlite3.Connection, source: str,\n442\t external_id: str, key: str) -> dict | None:\n443\t \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n444\t row = con.execute(\n445\t \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n446\t (source, external_id)).fetchone()\n447\t if row and row[\"key\"] == key and row[\"payload\"]:\n448\t try:\n449\t return json.loads(row[\"payload\"])\n450\t except ValueError:\n451\t return None\n452\t return None\n453\t\n454\t\n455\tdef put_cached_detail(con: sqlite3.Connection, source: str,\n456\t external_id: str, key: str, payload: dict) -> None:\n457\t con.execute(\n458\t \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n459\t \" VALUES (?,?,?,?,?)\"\n460\t \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n461\t \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n462\t (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n463\t time.time()))\n464\t con.commit()\n465\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e6ea831e-b959-4758-9bb7-bb1df05be1bd","timestamp":"2026-08-24T21:44:55.529Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls /Users/simon-pierreboucher/apps/lou-ka/louka/c…","usage":{"total_tokens":26944,"tool_uses":11,"duration_ms":12803},"last_tool_name":"Bash","uuid":"bbc24188-962c-403e-b75d-505cac7341a2","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJerDvqjVNebhqKw9FK","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KHdHz7iofmYksaHfZdkBp3","name":"Bash","input":{"command":"ls /Users/simon-pierreboucher/apps/lou-ka/louka/connectors | head -30"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":4325,"cache_read_input_tokens":22574,"cache_creation":{"ephemeral_5m_input_tokens":4325,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4a09245b-307b-419e-9105-8af33f6b8ede"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KHdHz7iofmYksaHfZdkBp3","type":"tool_result","content":"__init__.py\n__pycache__\n_detailutil.py\n_livya.py\n_resilient.py\naalto.py\nacceslogis_gb.py\nagence_sherbrooke.py\nakelius.py\nalexcellence.py\nalias_appartements.py\nalouer_sherbrooke.py\napartments_com.py\napp_urbains.py\nappartements_rimouski.py\nascensio.py\natimco.py\natlas_immo.py\naula.py\naxia.py\nbarnes_quebec.py\nbase.py\nbeaudoin.py\nbeaulieu_groupe.py\nbestlife.py\nboardwalk.py\nbons_locataires.py\nboreal_abitibi.py\nbribourg.py\nbrigil.py","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"16919c39-4b91-4444-8d83-72648d09b9d1","timestamp":"2026-08-24T21:44:58.130Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/connectors/__init__.py","usage":{"total_tokens":27250,"tool_uses":12,"duration_ms":14134},"last_tool_name":"Read","uuid":"bb96bfb8-ac5e-4d26-93b5-4ec2070ba460","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJf3ExUZofLmfHxSL6M","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016aQ13bV2S1jDk9rWYjKEMq","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/connectors/__init__.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":302,"cache_read_input_tokens":26899,"cache_creation":{"ephemeral_5m_input_tokens":302,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e7af063b-9bd6-45e5-bf68-51e4bcad839e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016aQ13bV2S1jDk9rWYjKEMq","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# connectors/__init__.py : registre AUTO-DÉCOUVRANT des connecteurs\n5\t# Tout module de ce paquet contenant une sous-classe de BaseConnector avec un\n6\t# source_id non vide est enregistré automatiquement — aucun fichier partagé\n7\t# à modifier pour ajouter un connecteur.\n8\t# -----------------------------------------------------------------------------\n9\tfrom __future__ import annotations\n10\t\n11\timport importlib\n12\timport pkgutil\n13\timport sys\n14\t\n15\tfrom .base import BaseConnector\n16\t\n17\tCONNECTORS: dict[str, type[BaseConnector]] = {}\n18\t\n19\tfor _mod in pkgutil.iter_modules(__path__):\n20\t if _mod.name in (\"base\", \"__init__\"):\n21\t continue\n22\t try:\n23\t module = importlib.import_module(f\"{__name__}.{_mod.name}\")\n24\t except Exception as exc: # un connecteur cassé ne bloque pas les autres\n25\t print(f\"[lou-ka] connecteur '{_mod.name}' ignoré : {exc}\", file=sys.stderr)\n26\t continue\n27\t for obj in vars(module).values():\n28\t if (isinstance(obj, type) and issubclass(obj, BaseConnector)\n29\t and obj is not BaseConnector and getattr(obj, \"source_id\", \"\")):\n30\t if getattr(obj, \"disabled\", False): # source hors-ligne (voir le module)\n31\t continue\n32\t CONNECTORS[obj.source_id] = obj\n33\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"33e682a5-ff80-41ae-bfe6-e0d1ed9b0e12","timestamp":"2026-08-24T21:44:59.434Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/fro…","usage":{"total_tokens":27252,"tool_uses":13,"duration_ms":14487},"last_tool_name":"Bash","uuid":"3a4872b0-155c-4421-bf80-8df90a8e98ff","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJf3ExUZofLmfHxSL6M","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KPjbAeBg6PsKrCJAgT9jk8","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/frontend"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":302,"cache_read_input_tokens":26899,"cache_creation":{"ephemeral_5m_input_tokens":302,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"df32147b-fb45-448d-9933-dbd0c54f2786"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading frontend/package.json","usage":{"total_tokens":27254,"tool_uses":14,"duration_ms":14493},"last_tool_name":"Read","uuid":"96d3fca7-cdb5-486b-84af-ce11a7986ddb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJf3ExUZofLmfHxSL6M","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012cwMgRzpEsf21HuBFxszWb","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/package.json"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":302,"cache_read_input_tokens":26899,"cache_creation":{"ephemeral_5m_input_tokens":302,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b641d660-1c42-458b-b437-c19d1ee5fe60"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012cwMgRzpEsf21HuBFxszWb","type":"tool_result","content":"1\t{\n2\t \"name\": \"lou-ka-frontend\",\n3\t \"author\": \"Simon-Pierre Boucher \",\n4\t \"private\": true,\n5\t \"version\": \"1.0.0\",\n6\t \"type\": \"module\",\n7\t \"scripts\": {\n8\t \"dev\": \"vite\",\n9\t \"build\": \"tsc -b && vite build\",\n10\t \"preview\": \"vite preview\"\n11\t },\n12\t \"dependencies\": {\n13\t \"react\": \"^18.3.1\",\n14\t \"react-dom\": \"^18.3.1\",\n15\t \"react-router-dom\": \"^6.26.0\",\n16\t \"@groupe-ka/ka-maps\": \"file:../../ka-maps\",\n17\t \"mapbox-gl\": \"^3.28.1\"\n18\t },\n19\t \"devDependencies\": {\n20\t \"@types/react\": \"^18.3.3\",\n21\t \"@types/react-dom\": \"^18.3.0\",\n22\t \"@vitejs/plugin-react\": \"^4.3.1\",\n23\t \"typescript\": \"^5.5.4\",\n24\t \"vite\": \"^5.4.0\"\n25\t }\n26\t}\n27\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e72bc0b3-38be-411b-9539-b26cbfffa8e3","timestamp":"2026-08-24T21:44:59.793Z"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KPjbAeBg6PsKrCJAgT9jk8","type":"tool_result","content":"total 184\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 16:51 .\ndrwxr-xr-x 27 simon-pierreboucher staff 864 Aug 24 16:22 ..\n-rw-r--r-- 1 simon-pierreboucher staff 6148 Aug 9 20:49 .DS_Store\ndrwxr-xr-x 11 simon-pierreboucher staff 352 Aug 24 16:47 dist\n-rw-r--r-- 1 simon-pierreboucher staff 2105 Aug 22 14:46 index.html\ndrwxr-xr-x@ 48 simon-pierreboucher staff 1536 Aug 17 20:26 node_modules\n-rw-r--r--@ 1 simon-pierreboucher staff 61833 Aug 17 20:27 package-lock.json\n-rw-r--r-- 1 simon-pierreboucher staff 626 Aug 13 03:13 package.json\ndrwxr-xr-x 9 simon-pierreboucher staff 288 Aug 24 01:17 public\ndrwxr-xr-x 3 simon-pierreboucher staff 96 Aug 19 19:57 scripts\ndrwxr-xr-x 15 simon-pierreboucher staff 480 Aug 24 16:47 src\n-rw-r--r-- 1 simon-pierreboucher staff 655 Aug 13 02:40 tsconfig.json\n-rw-r--r--@ 1 simon-pierreboucher staff 1776 Aug 24 16:47 tsconfig.tsbuildinfo\n-rw-r--r-- 1 simon-pierreboucher staff 898 Aug 13 03:13 vite.config.ts","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2175629e-4974-4fee-8ea1-70658125b7d1","timestamp":"2026-08-24T21:44:59.815Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/fro…","usage":{"total_tokens":29100,"tool_uses":15,"duration_ms":15886},"last_tool_name":"Bash","uuid":"eeda525e-f814-4aac-9dea-4c1a669bd937","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfASoC7wTGN1E7Pkwt","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NpDAebGGdC6uAF2s9owGWC","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/frontend/src"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":451,"cache_creation_input_tokens":1397,"cache_read_input_tokens":27201,"cache_creation":{"ephemeral_5m_input_tokens":1397,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"099784c3-fce1-47e5-81fb-333574d16602"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NpDAebGGdC6uAF2s9owGWC","type":"tool_result","content":"total 344\ndrwxr-xr-x 15 simon-pierreboucher staff 480 Aug 24 16:47 .\ndrwxr-xr-x 14 simon-pierreboucher staff 448 Aug 24 16:51 ..\n-rw-r--r--@ 1 simon-pierreboucher staff 14807 Aug 24 01:18 App.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 4894 Aug 15 15:48 Logo.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2212 Aug 13 00:34 account.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 24485 Aug 24 16:23 api.ts\ndrwxr-xr-x 26 simon-pierreboucher staff 832 Aug 24 16:23 components\n-rw-r--r--@ 1 simon-pierreboucher staff 3723 Aug 23 09:31 ctapi.ts\ndrwxr-xr-x 7 simon-pierreboucher staff 224 Aug 17 21:07 ka\ndrwxr-xr-x 6 simon-pierreboucher staff 192 Aug 22 19:43 kamaps\n-rw-r--r--@ 1 simon-pierreboucher staff 682 Aug 17 20:25 main.tsx\ndrwxr-xr-x 22 simon-pierreboucher staff 704 Aug 24 16:23 pages\ndrwxr-xr-x 4 simon-pierreboucher staff 128 Aug 22 16:15 search\n-rw-r--r--@ 1 simon-pierreboucher staff 107043 Aug 24 16:47 styles.css\n-rw-r--r-- 1 simon-pierreboucher staff 38 Aug 8 19:03 vite-env.d.ts","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b0cc8fbc-de78-4ca3-9c7c-48010181b011","timestamp":"2026-08-24T21:45:01.209Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/web.py","usage":{"total_tokens":29728,"tool_uses":16,"duration_ms":17279},"last_tool_name":"Read","uuid":"b7a84ddf-0321-450e-a521-c9943ca72569","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfGK275oZV9LbToRdZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XgTZQoRfLVjxShrN4xL6Wi","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","limit":150},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1069,"cache_read_input_tokens":28598,"cache_creation":{"ephemeral_5m_input_tokens":1069,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"13b224ad-8929-43aa-8991-00e7751a7808"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XgTZQoRfLVjxShrN4xL6Wi","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# web.py : API FastAPI (JSON) + service du frontend React (frontend/dist)\n5\t# -----------------------------------------------------------------------------\n6\tfrom __future__ import annotations\n7\t\n8\timport json\n9\timport threading\n10\tfrom pathlib import Path\n11\t\n12\tfrom fastapi import BackgroundTasks, Body, FastAPI, HTTPException, Query\n13\tfrom fastapi.middleware.cors import CORSMiddleware\n14\tfrom fastapi.middleware.gzip import GZipMiddleware\n15\tfrom fastapi.responses import FileResponse, RedirectResponse\n16\tfrom fastapi.staticfiles import StaticFiles\n17\t\n18\tfrom . import auth, db, ingest\n19\t\n20\tROOT = Path(__file__).resolve().parent.parent\n21\tSOURCES_PATH = ROOT / \"data\" / \"sources.json\"\n22\tFRONTEND_DIST = ROOT / \"frontend\" / \"dist\"\n23\t\n24\tapp = FastAPI(title=\"Lou-Ka API\", version=\"1.0\",\n25\t description=\"Agrégateur de logements à louer — province de Québec\")\n26\tapp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n27\t allow_methods=[\"*\"], allow_headers=[\"*\"])\n28\tapp.add_middleware(GZipMiddleware, minimum_size=1000)\n29\t\n30\t_sync_lock = threading.Lock()\n31\t\n32\t# comptes utilisateurs (connexion Google) — voir louka/auth.py\n33\tapp.include_router(auth.router)\n34\t\n35\t# rôles, favoris, pages gestionnaires — voir louka/accounts.py\n36\tfrom . import accounts # noqa: E402 (import tardif : évite le cycle web<->accounts)\n37\tapp.include_router(accounts.router)\n38\t\n39\t# personnalisation du profil (bio, réseaux sociaux, photo) — louka/profile.py\n40\tfrom . import profile as user_profile # noqa: E402\n41\tapp.include_router(user_profile.router)\n42\t\n43\t# section « Location court terme » — sous-système isolé (louka/shortterm/)\n44\tfrom .shortterm import web as ct_web # noqa: E402\n45\tapp.include_router(ct_web.router)\n46\t\n47\t# fichiers téléversés (photos de profil) — data/uploads/\n48\t_UPLOADS = ROOT / \"data\" / \"uploads\"\n49\t_UPLOADS.mkdir(parents=True, exist_ok=True)\n50\tapp.mount(\"/uploads\", StaticFiles(directory=_UPLOADS), name=\"uploads\")\n51\t\n52\t\n53\tdef _row_to_dict(row) -> dict:\n54\t d = dict(row)\n55\t d[\"amenities\"] = json.loads(d.get(\"amenities\") or \"[]\")\n56\t d[\"images\"] = json.loads(d.get(\"images\") or \"[]\")\n57\t # galerie nettoyée par le contrôle qualité (imgcheck.py) : liens morts,\n58\t # minuscules, placeholders et doublons retirés — prime sur la galerie brute\n59\t imgs_ok = d.pop(\"images_ok\", None)\n60\t if imgs_ok:\n61\t try:\n62\t cleaned = json.loads(imgs_ok)\n63\t if isinstance(cleaned, list):\n64\t d[\"images\"] = cleaned\n65\t except (ValueError, TypeError):\n66\t pass\n67\t d.pop(\"img_audit\", None)\n68\t d[\"details\"] = json.loads(d.get(\"details\") or \"{}\")\n69\t # autres plateformes où l'annonce est aussi publiée (badge « aussi sur… »)\n70\t if d.get(\"dup_sources\"):\n71\t try:\n72\t d[\"dup_sources\"] = json.loads(d[\"dup_sources\"])\n73\t except (ValueError, TypeError):\n74\t d[\"dup_sources\"] = []\n75\t if d.get(\"furnished\") is not None:\n76\t d[\"furnished\"] = bool(d[\"furnished\"])\n77\t return d\n78\t\n79\t\n80\t@app.get(\"/api/listings\")\n81\tdef list_listings(\n82\t city: str | None = None,\n83\t sector: str | None = None,\n84\t unit_type: str | None = None,\n85\t source: str | None = None,\n86\t price_max: float | None = None,\n87\t price_min: float | None = None,\n88\t pets: str | None = None, # \"oui\" -> oui OU conditions\n89\t furnished: int | None = None, # 1 / 0\n90\t available_by: str | None = None, # ISO : dispo maintenant ou avant date\n91\t area_min: float | None = None, # superficie minimale (pi²)\n92\t q: str | None = None,\n93\t deal: str | None = None, # sous | marche | sur (fair value)\n94\t kascore_min: float | None = None, # KA Score global minimal\n95\t sort: str | None = None, # \"deal\" affaires | \"ka\" KA Score\n96\t active: int = 1,\n97\t limit: int = Query(500, le=2000),\n98\t offset: int = 0,\n99\t):\n100\t con = db.connect()\n101\t sql = (\"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n102\t \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n103\t \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n104\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n105\t \" WHERE dup_of IS NULL\") # doublons masqués\n106\t args: list = []\n107\t if active in (0, 1):\n108\t sql += \" AND active=?\"; args.append(active)\n109\t if active == 1: # quarantaine qualité : jamais dans les résultats publics\n110\t sql += \" AND published=1\"\n111\t if city:\n112\t sql += \" AND city=?\"; args.append(city)\n113\t if sector:\n114\t sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n115\t if unit_type:\n116\t sql += \" AND unit_type=?\"; args.append(unit_type)\n117\t if source:\n118\t sql += \" AND source=?\"; args.append(source)\n119\t if price_max is not None:\n120\t sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n121\t if price_min is not None:\n122\t sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n123\t if pets == \"oui\":\n124\t sql += \" AND pets IN ('oui','conditions')\"\n125\t elif pets:\n126\t sql += \" AND pets=?\"; args.append(pets)\n127\t if furnished in (0, 1):\n128\t sql += \" AND furnished=?\"; args.append(furnished)\n129\t if available_by:\n130\t # « now » trié après les dates ISO : comparaison explicite\n131\t sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n132\t \" AND availability_date<=?))\"\n133\t args.append(available_by)\n134\t if area_min is not None:\n135\t sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n136\t if q:\n137\t sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n138\t args += [f\"%{q}%\"] * 3\n139\t if deal in (\"sous\", \"marche\", \"sur\"):\n140\t sql += \" AND fv.verdict=?\"; args.append(deal)\n141\t if kascore_min is not None:\n142\t sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n143\t args.append(kascore_min)\n144\t total = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n145\t if sort == \"deal\": # meilleures affaires d'abord (écart le plus négatif)\n146\t sql += (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n147\t \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n148\t elif sort == \"ka\": # meilleurs emplacements d'abord (KA Score global)\n149\t sql += (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n150\t \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"65a518e9-7ed6-441b-8919-d8b8f2b7099d","timestamp":"2026-08-24T21:45:02.579Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading frontend/src/api.ts","usage":{"total_tokens":29730,"tool_uses":17,"duration_ms":17416},"last_tool_name":"Read","uuid":"9b5e4300-0b75-4887-9112-6d4c2eaa7492","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfGK275oZV9LbToRdZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014DwLViPXbEp9J6Z5GRcv48","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","limit":150},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1069,"cache_read_input_tokens":28598,"cache_creation":{"ephemeral_5m_input_tokens":1069,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9feb99dd-d96f-43d8-8e23-ca9a0428395d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014DwLViPXbEp9J6Z5GRcv48","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// api.ts : types + client API robuste (timeout, erreurs typées)\n5\t// -----------------------------------------------------------------------------\n6\t\n7\texport interface ListingDetails {\n8\t inclusions?: Record;\n9\t appliances?: Record;\n10\t parking?: { available?: boolean; type?: string; included?: boolean; price?: number };\n11\t contact?: { phone?: string; email?: string };\n12\t ac?: boolean;\n13\t elevator?: boolean;\n14\t balcony?: boolean;\n15\t pool?: boolean;\n16\t gym?: boolean;\n17\t laundry?: boolean;\n18\t storage?: boolean;\n19\t smoking?: boolean;\n20\t floor?: number;\n21\t price_from?: boolean;\n22\t}\n23\t\n24\texport interface Poi {\n25\t cat: string; // epicerie, pharmacie, ecole, parc, bus…\n26\t name: string;\n27\t dist_m: number;\n28\t}\n29\t\n30\texport interface Digest {\n31\t version: number;\n32\t texte_nettoye: string;\n33\t en_bref: string | null;\n34\t sections: { titre: string; texte: string }[];\n35\t faits: {\n36\t prix_mensuel: number | null;\n37\t date_disponibilite: string | null;\n38\t duree_bail_minimale_mois: number | null;\n39\t nb_occupants_total: number | null;\n40\t salle_de_bain: \"commune\" | \"privee\" | null;\n41\t cuisine: \"commune\" | \"privee\" | null;\n42\t electromenagers: string[];\n43\t inclusions: string[];\n44\t contraintes: string[];\n45\t depot_mentionne: string | null;\n46\t quartier_mentionne: string | null;\n47\t };\n48\t confiance: Record;\n49\t incoherences: string[];\n50\t completude: number;\n51\t}\n52\t\n53\texport interface Quartier {\n54\t dauid?: string | null;\n55\t demographie?: {\n56\t population: number | null;\n57\t densite: number | null;\n58\t age_median: number | null;\n59\t revenu_median: number | null;\n60\t pct_locataires: number | null;\n61\t loyer_moyen: number | null;\n62\t pct_francais: number | null;\n63\t pct_univ: number | null;\n64\t };\n65\t proximite?: Record; // scores 0..1 (PMD StatCan)\n66\t defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };\n67\t chaleur?: { classe: number; ecart: number | null }; // 1 fraîcheur … 9 chaleur\n68\t crime?:\n69\t | { type: \"points\"; rayon_m: number; douze_mois: number; douze_mois_precedents: number;\n70\t categories?: { nom: string; n: number; n_prec: number }[] }\n71\t | { type: \"igc\"; ville: string; annee: number; indice: number; indice_canada: number | null };\n72\t}\n73\t\n74\texport interface Listing {\n75\t uid: string;\n76\t source: string;\n77\t external_id: string;\n78\t url: string;\n79\t title: string;\n80\t address: string;\n81\t sector: string;\n82\t city: string;\n83\t unit_type: string;\n84\t price: number | null;\n85\t price_label: string;\n86\t availability: string;\n87\t availability_date: string | null; // ISO \"2026-07-01\" ou \"now\"\n88\t area_sqft: number | null;\n89\t pets: string | null; // \"oui\" | \"non\" | \"conditions\"\n90\t furnished: boolean | null;\n91\t description: string;\n92\t amenities: string[];\n93\t details: ListingDetails;\n94\t images: string[];\n95\t lat: number | null;\n96\t lng: number | null;\n97\t bedrooms: number | null;\n98\t poi?: Poi[]; // commodités de proximité (fiche seulement)\n99\t quartier?: Quartier | null; // stats de quartier (fiche seulement)\n100\t digest?: Digest | null; // description structurée (fiche seulement)\n101\t price_history?: { ts: number; price: number | null }[];\n102\t first_seen?: number;\n103\t last_seen: number;\n104\t updated_at: number;\n105\t active: number;\n106\t // juste valeur locative (fair value) — louka/fairvalue.py\n107\t fv?: number | null; // valeur estimée ($/mois)\n108\t fv_low?: number | null; // fourchette basse\n109\t fv_high?: number | null; // fourchette haute\n110\t fv_deviation?: number | null; // (prix - fv) / fv\n111\t fv_verdict?: \"sous\" | \"marche\" | \"sur\" | null;\n112\t fv_confidence?: \"fort\" | \"moyen\" | \"faible\" | null;\n113\t // KA Scores (0-100, null = données insuffisantes / non desservi)\n114\t ks_walk?: number | null;\n115\t ks_transit?: number | null;\n116\t ks_bike?: number | null;\n117\t ks_calme?: number | null;\n118\t ks_services?: number | null;\n119\t ks_global?: number | null;\n120\t kascores?: KaScores | null; // détail complet (fiche seulement)\n121\t}\n122\t\n123\t// --- KA Scores — famille de scores maison Lou-Ka (louka/kascores.py) --------\n124\texport interface KaScores {\n125\t walk: number | null;\n126\t transit: number | null;\n127\t bike: number | null;\n128\t calme: number | null;\n129\t services: number | null;\n130\t global: number | null;\n131\t details: {\n132\t walk?: { cats?: { cat: string; dist_m: number | null; pts: number }[]; bonus_choix?: number; raison?: string };\n133\t transit?: { arret_bus_m?: number | null; station_metro_m?: number | null; pmd_percentile?: number | null; raison?: string };\n134\t bike?: { km_cyclables_1km?: number; note?: string; raison?: string };\n135\t calme?: { sources_bruit?: { source: string; dist_m: number | null; pen: number }[]; bonus_parc?: number; note?: string };\n136\t services?: { familles?: Record; raison?: string };\n137\t labels?: Record;\n138\t };\n139\t version: string;\n140\t computed_at: number;\n141\t}\n142\t\n143\texport interface KaScoresStats {\n144\t annonces: number;\n145\t avec_score: number;\n146\t couverture_pct: number;\n147\t version: string;\n148\t moyennes: Record;\n149\t}\n150\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e5055ce7-3cf1-4ee2-a2eb-7243e8629e09","timestamp":"2026-08-24T21:45:02.715Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/quality.py","usage":{"total_tokens":34466,"tool_uses":18,"duration_ms":19090},"last_tool_name":"Read","uuid":"03365334-577f-4bc7-a332-4931f31edc0a","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfNqfuN6SfU4gE5mNC","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Cf64EJUTpxtAwTdQuAYBTb","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/quality.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4731,"cache_read_input_tokens":29667,"cache_creation":{"ephemeral_5m_input_tokens":4731,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d1711880-e759-4ed5-bd4a-bbadb5028c59"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Cf64EJUTpxtAwTdQuAYBTb","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# quality.py : score de complétude par annonce + seuil minimum de publication\n5\t# Une annonce sous le seuil (sans prix, sans localisation ou sans contenu\n6\t# exploitable) est mise en QUARANTAINE (published=0) : conservée en base,\n7\t# re-synchronisée normalement, mais jamais affichée sur le site.\n8\t# -----------------------------------------------------------------------------\n9\tfrom __future__ import annotations\n10\t\n11\timport json\n12\timport re\n13\t\n14\t# Seuil de publication (score 0–100). Les portes dures (prix, localisation,\n15\t# contenu) s'appliquent en plus du score — voir evaluate().\n16\tPUBLISH_THRESHOLD = 40\n17\t\n18\t# Pondération du score de complétude (total = 100)\n19\tW_PRICE = 20 # prix mensuel exploitable\n20\tW_LOCATION = 14 # adresse OU coordonnées GPS (partiel si ville seulement)\n21\tW_CITY = 4\n22\tW_DESCRIPTION = 14 # >= 200 caractères ; partiel si >= 60\n23\tW_IMAGES = 16 # 0 -> 0 ; 1 -> 6 ; 2-4 -> 11 ; >= 5 -> 16\n24\tW_UNIT = 8 # type d'unité ou nombre de chambres\n25\tW_AVAILABILITY = 5\n26\tW_AREA = 4\n27\tW_BATHROOMS = 3\n28\tW_AMENITIES = 5 # >= 3 commodités ; partiel si >= 1\n29\tW_DETAILS = 4 # >= 3 champs structurés\n30\tW_URL = 3 # lien vers l'annonce originale (contact / réservation)\n31\t\n32\t\n33\t# ---- pertinence : n'afficher que des LOGEMENTS à louer au mois --------------\n34\t# Certaines sources mêlent stationnements, garages, entreposage, locaux\n35\t# commerciaux, objets divers ou locations à la nuit — souvent à ~200 $/mois.\n36\t# Ces annonces passent le score de complétude mais ne sont pas des logements :\n37\t# on les met en quarantaine (published=0) avec une raison « hors sujet ».\n38\t\n39\t# vocabulaire logement (titre/description)\n40\t_RE_RES = re.compile(\n41\t r\"chambre|room|studio|appart|apartment|condo|loft|maison|house|\"\n42\t r\"logement|logis\\b|r[ée]sidences?\\b|chalet|penthouse|duplex|triplex|\"\n43\t r\"bachelor|colocation|\"\n44\t r\"sous-sol|basement|½|1/2|\\b\\d[.,]5\\b|\\bdemie?s?\\b|\\bbed(?:room)?s?\\b|\"\n45\t r\"\\bunit[ée]?s?\\b|\\bpi[eè]ces\\b\", re.I)\n46\t\n47\t# objet non résidentiel proposé à la location\n48\t_RE_NONRES = re.compile(\n49\t r\"stationnements?|parkings?|garages?|entrep[oô]ts?|entreposage|\"\n50\t r\"espaces? de rangement|places? de (?:stationnement|parking)|\"\n51\t r\"lockers?|cabanons?|remises?|\"\n52\t r\"loca(?:l|ux) commercia(?:l|ux)|espaces? commercia(?:l|ux)|\\bbureaux?\\b\",\n53\t re.I)\n54\t\n55\t# description qui COMMENCE par l'objet non résidentiel (l'annonce vend ça)\n56\t_RE_DESC_LEAD = re.compile(\n57\t r\"^\\W*(?:grande?s?|petite?s?|beaux?|belles?|beau|magnifiques?|superbes?|\"\n58\t r\"jolie?s?|\\d+)?\\s*\"\n59\t r\"(?:stationnements?|parkings?|garages?|entrep[oô]ts?|\"\n60\t r\"places? de (?:stationnement|parking)|espaces? de (?:rangement|stationnement)|\"\n61\t r\"bureaux?|loca(?:l|ux) (?:commercia(?:l|ux)|industriels?|professionnels?))\\b\",\n62\t re.I)\n63\t\n64\t# local commercial / bureau / industriel (titre ou description)\n65\t_RE_COMMERCIAL = re.compile(\n66\t r\"loca(?:l|ux)\\s+(?:commercia(?:l|ux)|industriels?|professionnels?)|\"\n67\t r\"espaces?\\s+(?:de\\s+)?bureaux?\\b|espaces?\\s+commercia(?:l|ux)|\"\n68\t r\"aménagé\\s+en\\s+(?:restaurant|commerce|boutique)|\"\n69\t r\"(?:usage|zonage|bail)\\s+commercial\", re.I)\n70\t\n71\t# « bureau » comme nom de rue (av. Jacques-Bureau…) — pas un local à louer\n72\t_RE_STREET_BUREAU = re.compile(\n73\t r\"(?:av(?:enue)?|rue|boul(?:evard)?|ch(?:emin)?|pl(?:ace)?)\\.?\\s+\"\n74\t r\"(?:\\w+[- ])?bureau\\b\", re.I)\n75\t\n76\t# location à la nuit / courte durée (le « loyer » n'est pas un prix mensuel)\n77\t_RE_NIGHTLY = re.compile(\n78\t r\"tarif par nuit|prix par nuit|par nuit et non par mois|\"\n79\t r\"location à la nuit|court terme seulement\", re.I)\n80\t\n81\t\n82\tdef relevance_reason(d: dict) -> str | None:\n83\t \"\"\"Retourne la raison de quarantaine si l'annonce n'est pas un logement\n84\t à louer au mois (stationnement, garage, entreposage, local commercial,\n85\t objet divers, location à la nuit), sinon None.\n86\t\n87\t Les seuils de prix évitent les faux positifs : un vrai logement dont le\n88\t titre mentionne « garage » (ex. « Garage intérieur chauffé » pour un 4½\n89\t à 2 250 $) reste publié, alors qu'un « garage à louer » à 195 $ tombe.\n90\t \"\"\"\n91\t title = (d.get(\"title\") or \"\").strip()\n92\t desc = (d.get(\"description\") or \"\").strip()\n93\t head = desc[:300]\n94\t price = d.get(\"price\")\n95\t priced = isinstance(price, (int, float))\n96\t unit = (d.get(\"unit_type\") or \"\").strip()\n97\t res_title = bool(_RE_RES.search(title))\n98\t res_text = res_title or bool(_RE_RES.search(head))\n99\t\n100\t # 1) le titre vend un stationnement/garage/local, sans vocabulaire logement\n101\t if _RE_NONRES.search(title) and not res_title \\\n102\t and not _RE_STREET_BUREAU.search(title) \\\n103\t and (not unit or not priced or price < 700):\n104\t return \"hors sujet : stationnement/garage/local (titre)\"\n105\t # 2) la description s'ouvre sur l'objet non résidentiel — attrape les\n106\t # titres trompeurs (« Appartement Garage à Louer » à 195 $) et les\n107\t # titres-adresses (« P158 1400 Boul René-Lévesque »)\n108\t if _RE_DESC_LEAD.match(desc) and (\n109\t not priced or price < 450\n110\t or (not res_title and \"à vendre\" in head.lower())):\n111\t return \"hors sujet : stationnement/garage/local (description)\"\n112\t # 3) prix de case de stationnement + aucun vocabulaire logement\n113\t if priced and price < 450 and not res_text and _RE_NONRES.search(head):\n114\t return \"hors sujet : stationnement/garage (prix + texte)\"\n115\t # 4) local commercial/bureau/industriel sans aucun vocabulaire logement\n116\t if not res_text and _RE_COMMERCIAL.search(f\"{title}\\n{head}\"):\n117\t return \"hors sujet : local commercial/bureau\"\n118\t # 5) prix impossible pour un logement + aucun signal logement dans le texte\n119\t # (case de stationnement à titre-adresse, objets divers…)\n120\t if priced and price < 300 and not res_text and unit != \"Chambre\":\n121\t return \"hors sujet : prix non résidentiel, aucun signal logement\"\n122\t # 6) prix à la nuit / courte durée affiché comme loyer mensuel\n123\t if priced and price < 600 and _RE_NIGHTLY.search(desc):\n124\t return \"hors sujet : location à la nuit / court terme\"\n125\t return None\n126\t\n127\t\n128\tdef _as_list(value) -> list:\n129\t if isinstance(value, list):\n130\t return value\n131\t if isinstance(value, str) and value:\n132\t try:\n133\t out = json.loads(value)\n134\t return out if isinstance(out, list) else []\n135\t except ValueError:\n136\t return []\n137\t return []\n138\t\n139\t\n140\tdef _as_dict(value) -> dict:\n141\t if isinstance(value, dict):\n142\t return value\n143\t if isinstance(value, str) and value:\n144\t try:\n145\t out = json.loads(value)\n146\t return out if isinstance(out, dict) else {}\n147\t except ValueError:\n148\t return {}\n149\t return {}\n150\t\n151\t\n152\tdef evaluate(d: dict) -> tuple[float, bool, list[str]]:\n153\t \"\"\"Évalue une annonce (dict aux clés du schéma listings).\n154\t\n155\t Retourne (score 0–100, publiable, raisons de quarantaine).\n156\t Accepte indifféremment les champs JSON sérialisés (rangée SQLite) ou\n157\t déjà décodés (Listing.asdict) — images/amenities/details.\n158\t \"\"\"\n159\t score = 0.0\n160\t reasons: list[str] = []\n161\t\n162\t # loyer mensuel plausible : hors bornes = donnée corrompue à la source\n163\t # (0,02 $, 1 $, prix « à la nuit »…) — traité comme absence de prix\n164\t price = d.get(\"price\")\n165\t has_price = isinstance(price, (int, float)) and 175 <= price <= 20000\n166\t if has_price:\n167\t score += W_PRICE\n168\t\n169\t address = (d.get(\"address\") or \"\").strip()\n170\t city = (d.get(\"city\") or \"\").strip()\n171\t sector = (d.get(\"sector\") or \"\").strip()\n172\t has_coords = d.get(\"lat\") is not None and d.get(\"lng\") is not None\n173\t if address or has_coords:\n174\t score += W_LOCATION\n175\t elif sector:\n176\t score += W_LOCATION * 0.5\n177\t if city:\n178\t score += W_CITY\n179\t\n180\t desc = (d.get(\"description\") or \"\").strip()\n181\t if len(desc) >= 200:\n182\t score += W_DESCRIPTION\n183\t elif len(desc) >= 60:\n184\t score += W_DESCRIPTION * 0.5\n185\t\n186\t images = _as_list(d.get(\"images\"))\n187\t n_img = len(images)\n188\t if n_img >= 5:\n189\t score += W_IMAGES\n190\t elif n_img >= 2:\n191\t score += 11\n192\t elif n_img == 1:\n193\t score += 6\n194\t\n195\t if (d.get(\"unit_type\") or \"\").strip() or d.get(\"bedrooms\") is not None:\n196\t score += W_UNIT\n197\t if d.get(\"availability_date\") or (d.get(\"availability\") or \"\").strip():\n198\t score += W_AVAILABILITY\n199\t if d.get(\"area_sqft\"):\n200\t score += W_AREA\n201\t if d.get(\"bathrooms\") is not None:\n202\t score += W_BATHROOMS\n203\t\n204\t amenities = _as_list(d.get(\"amenities\"))\n205\t if len(amenities) >= 3:\n206\t score += W_AMENITIES\n207\t elif len(amenities) >= 1:\n208\t score += W_AMENITIES * 0.5\n209\t\n210\t details = _as_dict(d.get(\"details\"))\n211\t if len(details) >= 3:\n212\t score += W_DETAILS\n213\t elif len(details) >= 1:\n214\t score += W_DETAILS * 0.5\n215\t\n216\t if (d.get(\"url\") or \"\").strip():\n217\t score += W_URL\n218\t\n219\t score = round(min(score, 100.0), 1)\n220\t\n221\t # ---- portes dures de publication ---------------------------------------\n222\t if not has_price:\n223\t reasons.append(\"sans prix\" if not price else f\"prix non plausible ({price:g} $)\")\n224\t if not (address or has_coords or city):\n225\t reasons.append(\"sans localisation\")\n226\t # contenu exploitable : description OU un minimum de structure\n227\t if len(desc) < 40 and not amenities and not (d.get(\"unit_type\") or \"\").strip() \\\n228\t and d.get(\"bedrooms\") is None:\n229\t reasons.append(\"sans contenu exploitable\")\n230\t # pertinence : pas un logement à louer au mois (stationnement, garage…)\n231\t rel = relevance_reason(d)\n232\t if rel:\n233\t reasons.append(rel)\n234\t if not reasons and score < PUBLISH_THRESHOLD:\n235\t reasons.append(f\"complétude {score:.0f} < seuil {PUBLISH_THRESHOLD}\")\n236\t\n237\t return score, not reasons, reasons\n238\t\n239\t\n240\tdef backfill(limit: int | None = None, verbose: bool = True) -> dict:\n241\t \"\"\"(Re)calcule completeness/published sur toutes les annonces actives.\"\"\"\n242\t from . import db\n243\t con = db.connect()\n244\t rows = con.execute(\n245\t \"SELECT uid, title, price, address, city, sector, lat, lng, description,\"\n246\t \" images, unit_type, bedrooms, bathrooms, availability,\"\n247\t \" availability_date, area_sqft, amenities, details, url\"\n248\t \" FROM listings WHERE active=1\"\n249\t + (f\" LIMIT {int(limit)}\" if limit else \"\")).fetchall()\n250\t pub = quar = 0\n251\t for r in rows:\n252\t score, ok, reasons = evaluate(dict(r))\n253\t con.execute(\n254\t \"UPDATE listings SET completeness=?, published=?, quality_reasons=?\"\n255\t \" WHERE uid=?\",\n256\t (score, int(ok), json.dumps(reasons, ensure_ascii=False) if reasons\n257\t else None, r[\"uid\"]))\n258\t if ok:\n259\t pub += 1\n260\t else:\n261\t quar += 1\n262\t con.commit()\n263\t con.close()\n264\t out = {\"evaluees\": len(rows), \"publiees\": pub, \"quarantaine\": quar}\n265\t if verbose:\n266\t print(f\"[lou-ka] quality: {out}\")\n267\t return out\n268\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"48716077-681f-4664-aa82-1d9bbc5dce8f","timestamp":"2026-08-24T21:45:04.391Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/fairvalue.py","usage":{"total_tokens":34468,"tool_uses":19,"duration_ms":19479},"last_tool_name":"Read","uuid":"ac108238-cf24-48b4-8734-735c57ff6d91","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfNqfuN6SfU4gE5mNC","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NfevndQnTnCsCqcY9Ax7q8","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/fairvalue.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4731,"cache_read_input_tokens":29667,"cache_creation":{"ephemeral_5m_input_tokens":4731,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5b2873a8-39ae-4cec-a0ab-353e7ce32084"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NfevndQnTnCsCqcY9Ax7q8","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# fairvalue.py : framework d'estimation de la JUSTE VALEUR locative (fair value)\n5\t#\n6\t# Moteur générique Groupe-KA « estimation de juste valeur » : les\n7\t# spécificités logement vivent dans CONFIG (segments, caractéristiques,\n8\t# seuils) — décliner sur immo-ka (biens en vente) ou auto-ka (véhicules)\n9\t# = fournir une autre CONFIG et une autre requête de chargement.\n10\t#\n11\t# Trois niveaux, du plus robuste au plus fin :\n12\t# N1 référence par segment de marché (ville[/quartier] × chambres),\n13\t# médiane + p25/p75, volume minimal sinon repli au niveau supérieur ;\n14\t# N2 ajustements multiplicatifs par caractéristiques (meublé, chauffage\n15\t# inclus, stationnement…) estimés SUR LES DONNÉES elles-mêmes ;\n16\t# N3 comparables kNN géographiques (grille spatiale, pondération\n17\t# gaussienne par la distance).\n18\t# Combinaison + couloir de sécurité autour du segment + indice de confiance.\n19\t# AUCUNE donnée inventée : segment trop rare => confiance faible / pas de\n20\t# verdict, jamais d'estimation à l'aveugle.\n21\t# -----------------------------------------------------------------------------\n22\tfrom __future__ import annotations\n23\t\n24\timport math\n25\timport re\n26\timport time\n27\tfrom statistics import median\n28\t\n29\tMODEL_VERSION = \"fv-logement-1.0\"\n30\t\n31\tCONFIG = {\n32\t # volume minimal d'annonces pour qu'un segment fasse référence\n33\t \"min_segment\": 8,\n34\t # comparables (niveau 3)\n35\t \"knn_k\": 12,\n36\t \"knn_min\": 4,\n37\t \"knn_radius_m\": 3000.0,\n38\t \"knn_bandwidth_m\": 800.0, # pondération gaussienne exp(-(d/bw)^2)\n39\t # combinaison : poids du kNN quand il est disponible\n40\t \"knn_weight\": 0.6,\n41\t # couloir de sécurité autour de la référence du segment\n42\t \"corridor\": (0.70, 1.30), # x p25 … x p75\n43\t # classification (écart relatif au marché)\n44\t \"seuil_sous\": -0.08,\n45\t \"seuil_sur\": 0.08,\n46\t # au-delà : donnée probablement corrompue (prix à la semaine, coquille…)\n47\t # -> pas de verdict plutôt qu'un verdict trompeur\n48\t \"verdict_dev_bounds\": (-0.50, 1.00),\n49\t # ajustement superficie : (aire/aire_type)^elasticité, borné\n50\t \"area_elasticity\": 0.35,\n51\t \"area_adj_bounds\": (0.80, 1.30),\n52\t # caractéristiques ajustables (chemin json_extract dans details, ou champ)\n53\t \"features\": {\n54\t \"meuble\": (\"field\", \"furnished\"),\n55\t \"chauffage\": (\"details\", \"$.inclusions.heating\"),\n56\t \"electricite\": (\"details\", \"$.inclusions.electricity\"),\n57\t \"stationnement\": (\"details\", \"$.parking.available\"),\n58\t \"climatisation\": (\"details\", \"$.ac\"),\n59\t \"balcon\": (\"details\", \"$.balcony\"),\n60\t \"ascenseur\": (\"details\", \"$.elevator\"),\n61\t \"piscine\": (\"details\", \"$.pool\"),\n62\t \"lave_vaisselle\": (\"details\", \"$.appliances.dishwasher\"),\n63\t },\n64\t # bornes de sanité des coefficients appris (un coef hors bornes = bruit)\n65\t \"coef_bounds\": (0.85, 1.35),\n66\t \"coef_min_pairs\": 60, # nb min d'annonces avec ET sans la carac.\n67\t}\n68\t\n69\t\n70\t# --------------------------------------------------------------- utilitaires\n71\t# location de CHAMBRE / colocation : segment à part — comparer une chambre\n72\t# au loyer d'un logement entier fabriquerait de fausses « bonnes affaires »\n73\t_CHAMBRE = re.compile(\n74\t r\"(chambre\\s+(à|a)\\s+louer|chambre\\s+dans|chambre\\s+disponible|\"\n75\t r\"chambre\\s+meubl|colocation|\\bcoloc\\b|room\\s+for\\s+rent|\"\n76\t r\"room\\s+in\\b|roommate|chambre\\s+(à|a)\\s+partager)\", re.IGNORECASE)\n77\t\n78\t# prix affiché non mensuel (semaine/jour/nuit) : l'écart serait trompeur\n79\t_NON_MENSUEL = re.compile(\n80\t r\"(/\\s*sem|par\\s+sem|semaine|hebdo|/\\s*jour|par\\s+jour|/\\s*nuit|\"\n81\t r\"par\\s+nuit|/\\s*wk|/\\s*week\\b|weekly|/\\s*night|nightly|/\\s*day\\b)\",\n82\t re.IGNORECASE)\n83\t\n84\t\n85\tdef _bed_group(bedrooms) -> str:\n86\t \"\"\"Groupe de chambres : 0 (studio), 1, 2, 3, 4+ — clé de segment.\"\"\"\n87\t if bedrooms is None:\n88\t return \"?\"\n89\t b = int(round(float(bedrooms)))\n90\t return \"4+\" if b >= 4 else str(max(0, b))\n91\t\n92\t\n93\tdef _pct(sorted_vals: list[float], q: float) -> float:\n94\t if not sorted_vals:\n95\t return 0.0\n96\t i = q * (len(sorted_vals) - 1)\n97\t lo, hi = int(math.floor(i)), int(math.ceil(i))\n98\t return sorted_vals[lo] + (sorted_vals[hi] - sorted_vals[lo]) * (i - lo)\n99\t\n100\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"96f384f7-bc90-4af7-8fdf-5afcaba21150","timestamp":"2026-08-24T21:45:04.780Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/kascores.py","usage":{"total_tokens":34470,"tool_uses":20,"duration_ms":19732},"last_tool_name":"Read","uuid":"58071840-d28d-4a2e-b563-173e5d8e0327","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfNqfuN6SfU4gE5mNC","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012H32h5GzwjZaNmhR17sM6C","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/kascores.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4731,"cache_read_input_tokens":29667,"cache_creation":{"ephemeral_5m_input_tokens":4731,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ebf83c2d-b65e-446d-b9cb-2b343b0d9272"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012H32h5GzwjZaNmhR17sM6C","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# kascores.py : les KA Scores — famille de scores maison 0-100 par immeuble.\n5\t#\n6\t# KA Walk Score marchabilité (besoins quotidiens à pied)\n7\t# KA Transit Score desserte en transport collectif\n8\t# KA Bike Score praticité du vélo\n9\t# KA Calme Score tranquillité estimée du secteur\n10\t# KA Services Score richesse des services (1 km / 3 km)\n11\t# KA Score global moyenne pondérée (personnalisable côté client)\n12\t#\n13\t# Sources : OpenStreetMap (© contributeurs OSM, ODbL) via environment.py,\n14\t# mesures de proximité StatCan (PMD 2021, quartier.db) pour le volet\n15\t# fréquence/qualité du transport collectif. AUCUNE donnée inventée :\n16\t# secteur sans données → NULL (« Données insuffisantes »), territoire sans\n17\t# arrêt → NULL transit (« Non desservi »), et chaque score expose son\n18\t# détail (details JSON) affiché sur la fiche.\n19\t#\n20\t# Honnêteté méthodologique (affichée sur /ka-scores) :\n21\t# · distances à vol d'oiseau × 1,3 (facteur réseau usuel), pas un routage ;\n22\t# · le Calme est une estimation d'environnement, pas une mesure sonore ;\n23\t# · le Bike ignore le dénivelé (v1) ;\n24\t# · barème versionné — tout changement recalcule l'ensemble du parc.\n25\t# -----------------------------------------------------------------------------\n26\tfrom __future__ import annotations\n27\t\n28\timport json\n29\timport math\n30\timport sqlite3\n31\timport time\n32\tfrom pathlib import Path\n33\t\n34\tfrom . import db\n35\tfrom .environment import load_tile\n36\tfrom .poi import TILE, _haversine_m, _tile_of\n37\t\n38\tVERSION = \"2026.08-v1\"\n39\tDETOUR = 1.3 # vol d'oiseau → distance de marche estimée\n40\tQUARTIER_DB = Path(__file__).resolve().parent.parent / \"data\" / \"quartier.db\"\n41\t\n42\t# --- barème Walk : (catégorie, poids, pleine note ≤ m, zéro au-delà de m) ---\n43\tWALK_BAREME = [\n44\t (\"epicerie\", 3.0, 400, 1600),\n45\t (\"pharmacie\", 2.0, 400, 1600),\n46\t (\"parc\", 2.0, 300, 1200),\n47\t (\"cafe\", 1.5, 300, 1200),\n48\t (\"ecole\", 1.5, 500, 1600),\n49\t (\"clinique\", 1.5, 600, 2400),\n50\t (\"garderie\", 1.0, 500, 1600),\n51\t (\"depanneur\", 1.0, 250, 1000),\n52\t (\"gym\", 1.0, 500, 2000),\n53\t (\"bibliotheque\", 1.0, 500, 2000),\n54\t]\n55\t\n56\t# --- barème Services : famille → (catégories, rayon m, saturation, poids) ---\n57\tSERVICES_BAREME = [\n58\t (\"commerces\", (\"epicerie\", \"depanneur\", \"cafe\"), 1000, 15, 0.35),\n59\t (\"sante\", (\"pharmacie\", \"clinique\", \"hopital\"), 3000, 8, 0.25),\n60\t (\"education\", (\"ecole\", \"garderie\", \"bibliotheque\"), 3000, 8, 0.20),\n61\t (\"loisirs\", (\"gym\", \"parc\"), 1000, 6, 0.20),\n62\t]\n63\t\n64\t# --- pondérations par défaut du score global (personnalisables client) ---\n65\tGLOBAL_POIDS = {\"walk\": 0.30, \"transit\": 0.20, \"bike\": 0.15,\n66\t \"calme\": 0.20, \"services\": 0.15}\n67\t\n68\tLABELS = [(85, \"Exceptionnel\"), (70, \"Excellent\"), (55, \"Très bon\"),\n69\t (40, \"Moyen\"), (0, \"Faible\")]\n70\t\n71\t\n72\tdef label(score: float | None) -> str | None:\n73\t if score is None:\n74\t return None\n75\t for seuil, lbl in LABELS:\n76\t if score >= seuil:\n77\t return lbl\n78\t return \"Faible\"\n79\t\n80\t\n81\t# ---------------------------------------------------------------------------\n82\t# Index spatial en grille (cellules ~0,01° ≈ 1,1 km) — recherches locales\n83\t# ---------------------------------------------------------------------------\n84\t\n85\tclass Grid:\n86\t def __init__(self, cell: float = 0.01) -> None:\n87\t self.cell = cell\n88\t self.cells: dict[tuple[int, int], list[tuple[float, float]]] = {}\n89\t\n90\t def add(self, lat: float, lng: float) -> None:\n91\t key = (int(lat // self.cell), int(lng // self.cell))\n92\t self.cells.setdefault(key, []).append((lat, lng))\n93\t\n94\t def near(self, lat: float, lng: float, radius_m: float):\n95\t \"\"\"Tous les points à ≤ radius_m (parcours des cellules voisines).\"\"\"\n96\t r_lat = radius_m / 111000.0\n97\t r_lng = radius_m / (111000.0 * max(0.2, math.cos(math.radians(lat))))\n98\t span = int(max(r_lat, r_lng) // self.cell) + 1\n99\t cy, cx = int(lat // self.cell), int(lng // self.cell)\n100\t for dy in range(-span, span + 1):\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"18cf33ba-19e2-4b6c-8a0b-6eec6611cf66","timestamp":"2026-08-24T21:45:05.038Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Searching for ^def |^class ","usage":{"total_tokens":42753,"tool_uses":21,"duration_ms":21835},"last_tool_name":"Grep","uuid":"43f1e63e-aaa9-4324-b24f-fcf0044cad91","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfYkEQBWkVRdndwTvN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01EEYrBsdtb7Zb1tNFXdq7mJ","name":"Grep","input":{"pattern":"^def |^class ","path":"/Users/simon-pierreboucher/apps/lou-ka/louka","type":"py","head_limit":200},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":8279,"cache_read_input_tokens":34398,"cache_creation":{"ephemeral_5m_input_tokens":8279,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e631acd2-8817-4a68-b1f5-9344399d2752"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EEYrBsdtb7Zb1tNFXdq7mJ","type":"tool_result","content":"Found 200 files limit: 200\nlouka/pdfgen.py\nlouka/tal.py\nlouka/web.py\nlouka/ingest.py\nlouka/connectors/beaudoin.py\nlouka/statsextra.py\nlouka/statsdash.py\nlouka/hydro.py\nlouka/commerces.py\nlouka/gaz.py\nlouka/air.py\nlouka/quartier.py\nlouka/rdl.py\nlouka/inondation.py\nlouka/connectors/devimco.py\nlouka/kapdf.py\nlouka/seo.py\nlouka/shortterm/web.py\nlouka/connectors/tri_logis.py\nlouka/shortterm/connectors/kijiji.py\nlouka/shortterm/connectors/expedia.py\nlouka/shortterm/connectors/gitespassant.py\nlouka/shortterm/connectors/tremblantliving.py\nlouka/shortterm/connectors/parcscanada.py\nlouka/shortterm/connectors/hebergementquebec.py\nlouka/shortterm/connectors/glampinghub.py\nlouka/shortterm/connectors/hipcamp.py\nlouka/shortterm/connectors/tripadvisor.py\nlouka/shortterm/connectors/corporatestays.py\nlouka/shortterm/connectors/blueground.py\nlouka/shortterm/connectors/sonder.py\nlouka/shortterm/connectors/sinistar.py\nlouka/shortterm/connectors/airbnb.py\nlouka/shortterm/connectors/monsieurchalets.py\nlouka/shortterm/connectors/bonjourquebec.py\nlouka/shortterm/connectors/sepaq.py\nlouka/shortterm/connectors/mcal.py\nlouka/shortterm/connectors/chaletsalouer.py\nlouka/shortterm/connectors/qldc.py\nlouka/shortterm/connectors/vrbo.py\nlouka/shortterm/connectors/chaletsauquebec.py\nlouka/shortterm/connectors/wechalet.py\nlouka/shortterm/connectors/chaletsarabais.py\nlouka/shortterm/connectors/chaletsquebec.py\nlouka/shortterm/connectors/rsvpchalets.py\nlouka/shortterm/connectors/locationschalets.py\nlouka/shortterm/connectors/chaletsdirect.py\nlouka/shortterm/connectors/booking.py\nlouka/shortterm/ingest.py\nlouka/shortterm/connectors/base.py\nlouka/shortterm/db.py\nlouka/shortterm/schema.py\nlouka/connectors/apartments_com.py\nlouka/connectors/fb_marketplace.py\nlouka/connectors/base.py\nlouka/connectors/_resilient.py\nlouka/quality.py\nlouka/db.py\nlouka/kascores.py\nlouka/environment.py\nlouka/geocode.py\nlouka/schema.py\nlouka/normalize.py\nlouka/connectors/kijiji.py\nlouka/connectors/sutton.py\nlouka/fairvalue.py\nlouka/connectors/cosoltec.py\nlouka/connectors/huma.py\nlouka/connectors/ferrovia.py\nlouka/connectors/lakle.py\nlouka/connectors/jutras.py\nlouka/connectors/zumper.py\nlouka/imgcheck.py\nlouka/connectors/summum.py\nlouka/connectors/royal_lepage.py\nlouka/connectors/groupe_theoret.py\nlouka/connectors/matinale.py\nlouka/connectors/garic.py\nlouka/connectors/urban_services.py\nlouka/connectors/quartier_plus.py\nlouka/connectors/msi.py\nlouka/connectors/logis_experts.py\nlouka/connectors/mellem.py\nlouka/connectors/quartier_sila.py\nlouka/connectors/groupe_maurice.py\nlouka/connectors/remax_quebec.py\nlouka/connectors/equinoxe_batimo.py\nlouka/connectors/heafey.py\nlouka/connectors/lespac.py\nlouka/connectors/medway.py\nlouka/connectors/kangalou.py\nlouka/dedup.py\nlouka/connectors/_livya.py\nlouka/connectors/atimco.py\nlouka/connectors/thrace.py\nlouka/connectors/simplex.py\nlouka/connectors/_detailutil.py\nlouka/connectors/duproprio.py\nlouka/connectors/logisquebec.py\nlouka/connectors/louer_ca.py\nlouka/connectors/rentals_ca.py\nlouka/connectors/groupe_dallaire.py\nlouka/connectors/capreit.py\nlouka/connectors/rivero.py\nlouka/connectors/trudel.py\nlouka/connectors/omhm.py\nlouka/connectors/bons_locataires.py\nlouka/connectors/roomies.py\nlouka/accounts.py\nlouka/hubfav.py\nlouka/hubprofile.py\nlouka/auth.py\nlouka/profile.py\nlouka/connectors/m_immobilier.py\nlouka/connectors/barnes_quebec.py\nlouka/connectors/proprio_direct.py\nlouka/connectors/ubee.py\nlouka/connectors/via_capitale.py\nlouka/connectors/luxuor.py\nlouka/connectors/majestic.py\nlouka/connectors/lequerre.py\nlouka/connectors/immeubles_pm.py\nlouka/connectors/fournelle.py\nlouka/connectors/oxford.py\nlouka/connectors/alias_appartements.py\nlouka/connectors/lambert.py\nlouka/connectors/leviu.py\nlouka/connectors/immeubles_bois.py\nlouka/connectors/habitations_sf.py\nlouka/connectors/immnasr.py\nlouka/connectors/terater.py\nlouka/connectors/gestion_jdm.py\nlouka/connectors/quanta.py\nlouka/connectors/gio.py\nlouka/connectors/louer_saguenay.py\nlouka/connectors/wandji.py\nlouka/connectors/alexcellence.py\nlouka/connectors/acceslogis_gb.py\nlouka/connectors/beaulieu_groupe.py\nlouka/connectors/moderno.py\nlouka/connectors/lelaureat.py\nlouka/connectors/morin.py\nlouka/marketstats.py\nlouka/connectors/immeubles_db.py\nlouka/connectors/lacite.py\nlouka/connectors/ascensio.py\nlouka/connectors/alouer_sherbrooke.py\nlouka/connectors/uptimo.py\nlouka/connectors/bestlife.py\nlouka/connectors/elk.py\nlouka/connectors/labonte.py\nlouka/connectors/floria.py\nlouka/connectors/montagnais.py\nlouka/connectors/siagi.py\nlouka/connectors/souleymane.py\nlouka/connectors/dynamic.py\nlouka/connectors/info_logement.py\nlouka/connectors/agence_sherbrooke.py\nlouka/connectors/projection_immo.py\nlouka/connectors/kass.py\nlouka/connectors/immeubles_dcl.py\nlouka/connectors/groupe_jacques.py\nlouka/connectors/aalto.py\nlouka/connectors/gestimmo_estrie.py\nlouka/connectors/immeubles_ouellet.py\nlouka/connectors/location_saguenay.py\nlouka/connectors/atlas_immo.py\nlouka/connectors/cite_immobilier.py\nlouka/connectors/gestion_isr.py\nlouka/connectors/boreal_abitibi.py\nlouka/connectors/girs.py\nlouka/connectors/osgoode.py\nlouka/connectors/groupe_robin.py\nlouka/connectors/novelo.py\nlouka/connectors/stb_immobilier.py\nlouka/connectors/logispro.py\nlouka/connectors/mkar.py\nlouka/connectors/gia_t.py\nlouka/connectors/le_baronet.py\nlouka/connectors/gestion_laprise.py\nlouka/connectors/gestion_legrand.py\nlouka/connectors/immeubles_aeb.py\nlouka/connectors/katasa.py\nlouka/connectors/logement_mauricie.py\nlouka/connectors/evoludev.py\nlouka/connectors/carat_immobilier.py\nlouka/connectors/appartements_rimouski.py\nlouka/connectors/habitations_jeanne.py\nlouka/connectors/immogex.py\nlouka/connectors/gestion_habitation.py\nlouka/connectors/hestia.py\nlouka/connectors/gestion_fauvel.py\nlouka/connectors/elite.py\nlouka/connectors/forsa.py\nlouka/connectors/may_bourg.py\nlouka/connectors/gestion_valco.py\nlouka/connectors/desmarais.py\nlouka/fixtures.py\nlouka/connectors/place_florimay.py\nlouka/connectors/le_st_georges.py"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bbb4bbaa-587f-4a88-bfd1-661dc39fddce","timestamp":"2026-08-24T21:45:07.172Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/tal.py","usage":{"total_tokens":45457,"tool_uses":22,"duration_ms":23532},"last_tool_name":"Read","uuid":"44b228f8-3012-4e00-a2ef-7223eb25181c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfhrAphBv9Ee3e6NJN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019ZPG1Wu5xvVpvQ59UV5Uod","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/tal.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2702,"cache_read_input_tokens":42677,"cache_creation":{"ephemeral_5m_input_tokens":2702,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3499ee7b-7122-4fca-94f8-3cb07abbefe1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019ZPG1Wu5xvVpvQ59UV5Uod","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# tal.py : historique au Tribunal administratif du logement (TAL / ex-RDL)\n5\t#\n6\t# Les décisions du TAL sont diffusées gratuitement par SOQUIJ\n7\t# (citoyens.soquij.qc.ca, tribunal no 35, plein texte depuis ~2009). Le site\n8\t# est protégé par une détection de bot (byscuit) : la RECHERCHE exige un\n9\t# rendu JS + IP résidentielle (Scrapfly, asp + render_js + session), mais la\n10\t# page d'une DÉCISION se charge ensuite sans rendu (même session = moins\n11\t# cher). Le moteur ne supporte pas les phrases entre guillemets : les mots\n12\t# sont combinés en ET implicite, d'où la stratégie\n13\t# recherche « »\n14\t# puis VÉRIFICATION de l'adresse exacte dans le texte intégral.\n15\t#\n16\t# Tout est mis en cache dans data/tal.db (base séparée : jamais de verrou\n17\t# sur louka.db) par adresse normalisée (no civique + rue + ville) :\n18\t# · tal_lookup : une ligne par adresse vérifiée (ou en file d'attente) ;\n19\t# · tal_decisions : décisions confirmées à l'adresse, classées par mots\n20\t# clés (non-paiement, résiliation, expulsion, reprise…)\n21\t# et par partie demanderesse (locateur vs locataire).\n22\t#\n23\t# Usage : python run.py tal [n] [budget_s] # précalcul par lots\n24\t# Lecture : lookup(address, city) — bloc « Historique TAL » de la fiche\n25\t# (/api/tal) ; une adresse jamais vérifiée est mise en file\n26\t# prioritaire et traitée au prochain passage du watch.\n27\t# -----------------------------------------------------------------------------\n28\tfrom __future__ import annotations\n29\t\n30\timport html\n31\timport json\n32\timport os\n33\timport re\n34\timport sqlite3\n35\timport threading\n36\timport time\n37\timport unicodedata\n38\timport urllib.parse\n39\timport urllib.request\n40\tfrom concurrent.futures import ThreadPoolExecutor, as_completed\n41\tfrom pathlib import Path\n42\t\n43\tDATA_DIR = Path(__file__).resolve().parent.parent / \"data\"\n44\tDB_PATH = DATA_DIR / \"tal.db\"\n45\tLOUKA_DB = DATA_DIR / \"louka.db\"\n46\t\n47\tSCRAPFLY_API = \"https://api.scrapfly.io/scrape\"\n48\tSOQUIJ_BASE = \"https://citoyens.soquij.qc.ca\"\n49\tTRIBUNAL_ID = \"35\" # Tribunal administratif du logement / RDL\n50\t\n51\tREFRESH_DAYS = 180 # revérifier une adresse après ~6 mois\n52\tERROR_RETRY_DAYS = 7 # réessayer plus vite après une erreur\n53\tMAX_DECISIONS = 12 # décisions lues au maximum par adresse\n54\tWORKERS = 4\n55\t\n56\t_SCHEMA = \"\"\"\n57\tCREATE TABLE IF NOT EXISTS tal_lookup (\n58\t addr_key TEXT PRIMARY KEY,\n59\t civic TEXT, street TEXT, city TEXT, query TEXT,\n60\t status TEXT, -- queued | ok | error\n61\t found INTEGER, -- résultats bruts SOQUIJ (avant vérification)\n62\t matched INTEGER, -- décisions confirmées à l'adresse\n63\t priority INTEGER DEFAULT 0, -- 1 = demandé par une fiche (file rapide)\n64\t checked_at TEXT,\n65\t error TEXT\n66\t);\n67\tCREATE TABLE IF NOT EXISTS tal_decisions (\n68\t addr_key TEXT,\n69\t decision_id TEXT, -- ID hexadécimal SOQUIJ\n70\t citation TEXT, -- ex. « 2026 QCTAL 21509 »\n71\t parties TEXT,\n72\t date TEXT,\n73\t demandeur TEXT, -- locateur | locataire | NULL (indéterminé)\n74\t tags TEXT, -- JSON : [\"non-paiement\", \"résiliation de bail\", …]\n75\t verdict TEXT, -- accueillie | rejetée | NULL\n76\t url TEXT,\n77\t PRIMARY KEY (addr_key, decision_id)\n78\t);\n79\t\"\"\"\n80\t\n81\t# ---------------------------------------------------------------------------\n82\t# Normalisation d'adresse\n83\t# ---------------------------------------------------------------------------\n84\t\n85\t_TYPES_VOIE = {\n86\t \"rue\", \"avenue\", \"av\", \"ave\", \"boulevard\", \"boul\", \"blvd\", \"bd\", \"chemin\",\n87\t \"ch\", \"place\", \"pl\", \"cote\", \"allee\", \"montee\", \"carre\", \"impasse\",\n88\t \"terrasse\", \"tsse\", \"croissant\", \"crois\", \"rang\", \"route\", \"rte\",\n89\t \"promenade\", \"prom\", \"voie\", \"square\", \"cours\", \"sentier\", \"parc\",\n90\t # types anglais (annonces montréalaises)\n91\t \"st\", \"street\", \"rd\", \"road\", \"dr\", \"drive\", \"cres\", \"crescent\",\n92\t \"ct\", \"court\", \"lane\", \"ln\", \"hwy\", \"way\",\n93\t}\n94\t_STOPWORDS = {\"de\", \"du\", \"des\", \"la\", \"le\", \"les\", \"l\", \"d\", \"a\", \"au\", \"aux\",\n95\t \"app\", \"apt\", \"unite\", \"suite\", \"bureau\", \"local\"}\n96\t\n97\t\n98\tdef _fold(s: str) -> str:\n99\t \"\"\"Minuscules sans accents (comparaisons et clés).\"\"\"\n100\t s = unicodedata.normalize(\"NFD\", s)\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0f287e79-4d45-48bf-af6b-44d7c84d32b1","timestamp":"2026-08-24T21:45:08.831Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/quartier.py","usage":{"total_tokens":45459,"tool_uses":23,"duration_ms":23851},"last_tool_name":"Read","uuid":"905438d5-435d-4128-a80c-43cc5bf847a7","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfhrAphBv9Ee3e6NJN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01EYTrs4dSeTaJSU33HDrfr6","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/quartier.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2702,"cache_read_input_tokens":42677,"cache_creation":{"ephemeral_5m_input_tokens":2702,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ccd4afb6-a761-4c35-b433-0c9dbe62bbf5"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EYTrs4dSeTaJSU33HDrfr6","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# quartier.py : statistiques de quartier par annonce (à la Centris, en libre)\n5\t# Base statique data/quartier.db construite par scripts/build_*.py :\n6\t# - da_poly / da_stats : aires de diffusion 2021 + profil du recensement\n7\t# - da_pmd : mesures de proximité StatCan (scores 0..1)\n8\t# - da_defav : défavorisation matérielle/sociale INSPQ (quintiles)\n9\t# - heat : classe d'îlot de chaleur/fraîcheur INSPQ par immeuble\n10\t# - crime_mtl / igc : actes criminels SPVM (points) + indice de gravité\n11\t# Jointure : lat/lng -> DAUID par point-dans-polygone local (préfiltre bbox),\n12\t# mémorisée dans listings.dauid à l'enrichissement (boucle watch).\n13\t# -----------------------------------------------------------------------------\n14\tfrom __future__ import annotations\n15\t\n16\timport json\n17\timport math\n18\timport sqlite3\n19\timport time\n20\tfrom pathlib import Path\n21\t\n22\tfrom . import db\n23\t\n24\tQUARTIER_DB = Path(__file__).resolve().parent.parent / \"data\" / \"quartier.db\"\n25\t\n26\t# villes couvertes par les points SPVM (agglomération de Montréal)\n27\t_VILLES_SPVM = {\"montreal\", \"montreal-est\", \"montreal-ouest\", \"westmount\",\n28\t \"cote saint-luc\", \"cote-saint-luc\", \"hampstead\", \"mont-royal\",\n29\t \"outremont\", \"verdun\", \"lasalle\", \"lachine\", \"anjou\",\n30\t \"saint-leonard\", \"saint-laurent\", \"ahuntsic\", \"dorval\",\n31\t \"pointe-claire\", \"kirkland\", \"beaconsfield\", \"dollard-des-ormeaux\"}\n32\t\n33\t# correspondance ville -> fragment du nom de service dans la table igc\n34\t_IGC_SERVICE = {\n35\t \"quebec\": \"SPVQ\", \"levis\": \"Lévis\", \"montreal\": \"SPVM\",\n36\t \"laval\": \"Laval\", \"longueuil\": \"Longueuil\",\n37\t}\n38\t\n39\t\n40\tdef disponible() -> bool:\n41\t return QUARTIER_DB.exists()\n42\t\n43\t\n44\tdef _connect() -> sqlite3.Connection:\n45\t con = sqlite3.connect(f\"file:{QUARTIER_DB}?mode=ro\", uri=True)\n46\t con.row_factory = sqlite3.Row\n47\t return con\n48\t\n49\t\n50\t# ---------------------------------------------------------------------------\n51\t# lat/lng -> DAUID (point dans polygone, préfiltre bbox)\n52\t# ---------------------------------------------------------------------------\n53\t\n54\tdef _dans_anneau(lat: float, lng: float, anneau: list) -> bool:\n55\t \"\"\"Lancer de rayon (even-odd). anneau = [[lng, lat], ...].\"\"\"\n56\t dedans = False\n57\t n = len(anneau)\n58\t j = n - 1\n59\t for i in range(n):\n60\t xi, yi = anneau[i][0], anneau[i][1]\n61\t xj, yj = anneau[j][0], anneau[j][1]\n62\t if (yi > lat) != (yj > lat) and \\\n63\t lng < (xj - xi) * (lat - yi) / (yj - yi + 1e-12) + xi:\n64\t dedans = not dedans\n65\t j = i\n66\t return dedans\n67\t\n68\t\n69\tdef dauid_for(qcon: sqlite3.Connection, lat: float, lng: float) -> str | None:\n70\t rows = qcon.execute(\n71\t \"SELECT dauid, poly FROM da_poly WHERE lat_min<=? AND lat_max>=?\"\n72\t \" AND lng_min<=? AND lng_max>=?\", (lat, lat, lng, lng)).fetchall()\n73\t for r in rows:\n74\t anneaux = json.loads(r[\"poly\"])\n75\t # even-odd sur tous les anneaux (les trous annulent)\n76\t compte = sum(1 for a in anneaux if _dans_anneau(lat, lng, a))\n77\t if compte % 2 == 1:\n78\t return r[\"dauid\"]\n79\t return None\n80\t\n81\t\n82\t# ---------------------------------------------------------------------------\n83\t# Assemblage pour la fiche\n84\t# ---------------------------------------------------------------------------\n85\t\n86\tdef _cle_ville(city: str) -> str:\n87\t import unicodedata\n88\t s = \"\".join(c for c in unicodedata.normalize(\"NFD\", city or \"\")\n89\t if unicodedata.category(c) != \"Mn\")\n90\t return s.strip().lower()\n91\t\n92\t\n93\tdef _crime_mtl(qcon: sqlite3.Connection, lat: float, lng: float) -> dict | None:\n94\t \"\"\"Comptage des actes criminels SPVM à < 500 m : 12 mois vs 12 précédents.\"\"\"\n95\t dlat = 500 / 111000.0\n96\t dlng = 500 / (111000.0 * max(0.2, math.cos(math.radians(lat))))\n97\t now = time.time()\n98\t rows = qcon.execute(\n99\t \"SELECT lat, lng, ts, categorie FROM crime_mtl WHERE lat BETWEEN ? AND ?\"\n100\t \" AND lng BETWEEN ? AND ? AND ts >= ?\",\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"909fec7b-9972-4d8d-86c9-2809d11f28c1","timestamp":"2026-08-24T21:45:09.150Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/rdl.py","usage":{"total_tokens":45461,"tool_uses":24,"duration_ms":24032},"last_tool_name":"Read","uuid":"44d50ecb-3091-4960-a3b1-d17640cae11c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfhrAphBv9Ee3e6NJN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01YP6Dpk1e6tUpGQzvokxQRW","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/rdl.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2702,"cache_read_input_tokens":42677,"cache_creation":{"ephemeral_5m_input_tokens":2702,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"219856bb-ccd6-437b-af4e-6a8638711af1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01YP6Dpk1e6tUpGQzvokxQRW","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# rdl.py : Registre des loyers (registre-des-loyers.ca) — extraction + requêtes\n5\t#\n6\t# Le Registre des loyers est une initiative citoyenne (Vivre en ville) où\n7\t# les locataires déclarent volontairement leur loyer. La carte publique\n8\t# expose une API JSON ouverte :\n9\t# GET /api/v1/housings/{NElat},{NElng},{SWlat},{SWlng}\n10\t# On balaie la province par grandes boîtes (aucune pagination côté serveur ;\n11\t# la boîte « tout le Québec » dépasse ses capacités -> découpage, avec\n12\t# subdivision récursive en cas d'erreur 500), puis on stocke le tout dans\n13\t# data/rdl.db (base séparée : jamais de verrou sur louka.db).\n14\t#\n15\t# Usage : python run.py rdl # rafraîchît la base locale\n16\t# Lecture : nearby(lat, lng, radius_m) # loyers déclarés autour d'un point\n17\t# (bloc « Registre des loyers » de la fiche, /api/rdl)\n18\t# -----------------------------------------------------------------------------\n19\tfrom __future__ import annotations\n20\t\n21\timport json\n22\timport math\n23\timport sqlite3\n24\timport time\n25\timport urllib.request\n26\tfrom pathlib import Path\n27\tfrom statistics import median\n28\t\n29\tDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"rdl.db\"\n30\tAPI = \"https://registre-des-loyers.ca/api/v1/housings/{},{},{},{}\"\n31\tUA = \"LouKaBot/1.0 (+https://www.lou-ka.com; contact@spboucher.ai)\"\n32\tDELAY = 2.0 # politesse entre deux boîtes\n33\t\n34\t# Boîtes (NE_lat, NE_lng, SW_lat, SW_lng) couvrant le pays entier (le registre\n35\t# est pancanadien ; Lou-Ka n'affiche que ce qui tombe près d'une annonce).\n36\t# Elles se recoupent légèrement, la déduplication se fait par id.\n37\tBOXES: list[tuple[float, float, float, float]] = [\n38\t (46.0, -70.0, 45.0, -75.0), # Grand Montréal + Montérégie + Estrie\n39\t (47.2, -70.0, 46.0, -76.0), # Québec, Mauricie, Centre-du-Québec\n40\t (46.2, -74.8, 45.2, -77.5), # Outaouais\n41\t (49.5, -63.5, 47.2, -80.0), # Abitibi, Saguenay, Gaspésie\n42\t (47.2, -63.5, 45.0, -70.0), # Bas-Saint-Laurent, Beauce est\n43\t (63.0, -55.0, 49.5, -80.0), # Côte-Nord, Nord-du-Québec\n44\t (45.05, -70.0, 44.5, -80.0), # frange frontalière sud\n45\t (57.0, -74.5, 41.6, -96.0), # Ontario\n46\t (60.0, -96.0, 48.9, -110.0), # Prairies (MB, SK)\n47\t (60.0, -110.0, 48.0, -140.0), # Alberta, Colombie-Britannique\n48\t (70.0, -60.0, 60.0, -142.0), # territoires + Nunavik\n49\t (49.0, -52.0, 43.0, -70.0), # Atlantique (NB, NÉ, ÎPÉ, TNL sud)\n50\t (61.0, -52.0, 49.0, -57.0), # Terre-Neuve nord + Labrador est\n51\t]\n52\t\n53\t# au-delà de ce volume on subdivise par prudence (plafond serveur inconnu)\n54\tSUSPECT = 40000\n55\t\n56\t_SCHEMA = \"\"\"\n57\tCREATE TABLE IF NOT EXISTS rdl_housings (\n58\t id INTEGER PRIMARY KEY,\n59\t full_address TEXT, street_number TEXT, apartment_number TEXT,\n60\t street_name TEXT, city TEXT, zip TEXT,\n61\t lat REAL, lng REAL,\n62\t price REAL, rooms INTEGER, year INTEGER, start_date TEXT,\n63\t type_of_accomodation TEXT,\n64\t heating_included INTEGER, electricity_included INTEGER,\n65\t furnishing_included INTEGER, parking_included INTEGER,\n66\t animal_allowed INTEGER,\n67\t address_slug TEXT, updated_at TEXT, fetched_at TEXT\n68\t);\n69\tCREATE INDEX IF NOT EXISTS idx_rdl_latlng ON rdl_housings (lat, lng);\n70\t\"\"\"\n71\t\n72\t\n73\tdef _connect(ro: bool = False) -> sqlite3.Connection:\n74\t if ro:\n75\t con = sqlite3.connect(f\"file:{DB_PATH}?mode=ro\", uri=True)\n76\t else:\n77\t DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n78\t con = sqlite3.connect(DB_PATH)\n79\t con.row_factory = sqlite3.Row\n80\t return con\n81\t\n82\t\n83\tdef _fetch_box(ne_lat: float, ne_lng: float, sw_lat: float, sw_lng: float,\n84\t depth: int = 0) -> list[dict]:\n85\t \"\"\"Une boîte ; en cas d'erreur serveur (boîte trop lourde), découpe en 4.\"\"\"\n86\t url = API.format(ne_lat, ne_lng, sw_lat, sw_lng)\n87\t req = urllib.request.Request(url, headers={\"User-Agent\": UA})\n88\t try:\n89\t with urllib.request.urlopen(req, timeout=120) as resp:\n90\t data = json.load(resp)\n91\t rows = data.get(\"data\", {}).get(\"housings\") or []\n92\t if len(rows) < SUSPECT or depth >= 3:\n93\t return rows\n94\t raise RuntimeError(f\"{len(rows)} résultats — subdivision de prudence\")\n95\t except Exception as exc:\n96\t if depth >= 3:\n97\t print(f\" ! abandon boîte {url}: {exc}\")\n98\t return []\n99\t mid_lat = (ne_lat + sw_lat) / 2\n100\t mid_lng = (ne_lng + sw_lng) / 2\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a97d73cf-4be9-46da-8bd4-2e4f0104375b","timestamp":"2026-08-24T21:45:09.331Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/dat…","usage":{"total_tokens":51029,"tool_uses":25,"duration_ms":25631},"last_tool_name":"Bash","uuid":"6136b29c-f86e-4174-a44c-6b622cb28f8d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfsCVVp8hbg7qh3ZvV","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UbgGzdi3saKhrp6nL8BLPs","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/data | head -40"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":5562,"cache_read_input_tokens":45379,"cache_creation":{"ephemeral_5m_input_tokens":5562,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d7459153-8dab-40ce-9d49-f5e785d53baa"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UbgGzdi3saKhrp6nL8BLPs","type":"tool_result","content":"total 4923056\ndrwxr-xr-x 23 simon-pierreboucher staff 736 Aug 24 17:36 .\ndrwxr-xr-x 27 simon-pierreboucher staff 864 Aug 24 16:22 ..\n-rw-r--r--@ 1 simon-pierreboucher staff 65536 Aug 23 22:14 air.db\n-rw-r--r--@ 1 simon-pierreboucher staff 106496 Aug 24 17:27 commerces.db\n-rw-r--r--@ 1 simon-pierreboucher staff 454656 Aug 24 14:34 gaz.db\n-rw-r--r--@ 1 simon-pierreboucher staff 32768 Aug 24 15:47 hydro.db\ndrwxr-xr-x@ 65535 simon-pierreboucher staff 2518976 Aug 24 17:26 imgcache\n-rw-r--r--@ 1 simon-pierreboucher staff 493109248 Aug 23 21:59 inondation.db\n-rw-r--r--@ 1 simon-pierreboucher staff 1471152128 Aug 24 17:43 louka.db\n-rw-r--r--@ 1 simon-pierreboucher staff 65536 Aug 24 16:58 louka.db-shm\n-rw-r--r--@ 1 simon-pierreboucher staff 28226152 Aug 24 17:45 louka.db-wal\n-rw-r--r-- 1 simon-pierreboucher staff 4947968 Aug 17 19:41 louka.db.backup-avant-m1\n-rw-r--r--@ 1 simon-pierreboucher staff 238264320 Aug 23 01:54 louka_ct.db\n-rw-r--r--@ 1 simon-pierreboucher staff 120573952 Aug 9 03:06 quartier.db\n-rw-r--r--@ 1 simon-pierreboucher staff 15548416 Aug 23 21:52 rdl.db\n-rw-r--r--@ 1 simon-pierreboucher staff 123105 Aug 22 18:20 sources.json\n-rw-r--r--@ 1 simon-pierreboucher staff 5295 Aug 23 09:30 sources_ct.json\n-rw-r--r-- 1 simon-pierreboucher staff 2256896 Aug 8 21:40 staging-contexte.db\n-rw-r--r--@ 1 simon-pierreboucher staff 3678208 Aug 9 03:06 staging-environnement.db\n-rw-r--r-- 1 simon-pierreboucher staff 113831936 Aug 8 23:33 staging-recensement.db\n-rw-r--r--@ 1 simon-pierreboucher staff 20480 Aug 24 17:26 tal.db\n-rw-r--r-- 1 simon-pierreboucher staff 3743744 Aug 23 22:35 transit.db\ndrwxr-xr-x@ 3 simon-pierreboucher staff 96 Aug 13 01:03 uploads","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"653b0a9b-9b4c-4ade-aff8-6cb446ec1d1e","timestamp":"2026-08-24T21:45:10.958Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading data/sources.json","usage":{"total_tokens":52013,"tool_uses":26,"duration_ms":27074},"last_tool_name":"Read","uuid":"78a1aab6-795d-47da-bd2a-2a4d0a12c1d7","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJfzFQNmjBNbW2W19oJ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0129ESkd3Nb8nx5n1FSKkgVa","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/data/sources.json","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":979,"cache_read_input_tokens":50941,"cache_creation":{"ephemeral_5m_input_tokens":979,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3360decd-e263-4b47-8af2-adf16fecd132"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0129ESkd3Nb8nx5n1FSKkgVa","type":"tool_result","content":"1\t{\n2\t \"_comment\": \"Lou-Ka — Registre des sources (agences/gestionnaires immobiliers, Québec & Lévis). Auteur : Simon-Pierre Boucher — contact@spboucher.ai\",\n3\t \"sources\": [\n4\t {\n5\t \"id\": \"lafrance_mathieu\",\n6\t \"name\": \"Lafrance & Mathieu\",\n7\t \"url\": \"https://lafrance-mathieu.com\",\n8\t \"listing_url\": \"https://lafrance-mathieu.com/louer-appartement-quebec\",\n9\t \"sectors\": \"Beauport, Charlesbourg, Limoilou, St-Roch, Lévis, Val-Bélair, Loretteville, L'Ancienne-Lorette, St-Augustin\",\n10\t \"connector\": \"lafrance_mathieu\",\n11\t \"status\": \"actif\",\n12\t \"region\": \"Québec\"\n13\t },\n14\t {\n15\t \"id\": \"msi\",\n16\t \"name\": \"MSI Gestion immobilière\",\n17\t \"url\": \"https://www.msimmobiliers.com\",\n18\t \"listing_url\": \"https://www.msimmobiliers.com/appartements-a-louer/quebec\",\n19\t \"sectors\": \"Ville de Québec, Lévis, Limoilou, Beauport, Ste-Foy, Vanier, St-Nicolas\",\n20\t \"connector\": \"msi\",\n21\t \"status\": \"actif\",\n22\t \"region\": \"Québec\"\n23\t },\n24\t {\n25\t \"id\": \"logisco\",\n26\t \"name\": \"Logisco\",\n27\t \"url\": \"https://logisco.com/fr\",\n28\t \"listing_url\": \"https://logisco.com/fr/appartements-a-louer\",\n29\t \"sectors\": \"Québec (Ste-Foy, Val-Bélair, Loretteville, Vanier, St-Augustin) + Lévis (St-Romuald, St-David, St-Nicolas, Desjardins)\",\n30\t \"connector\": \"logisco\",\n31\t \"status\": \"actif\",\n32\t \"region\": \"Québec\"\n33\t },\n34\t {\n35\t \"id\": \"laberge\",\n36\t \"name\": \"Groupe Laberge\",\n37\t \"url\": \"https://www.laberge.qc.ca\",\n38\t \"listing_url\": \"https://www.laberge.qc.ca/recherche\",\n39\t \"sectors\": \"Ste-Foy, Beauport, Vanier, L'Ancienne-Lorette, Limoilou\",\n40\t \"connector\": \"laberge\",\n41\t \"status\": \"actif\",\n42\t \"region\": \"Québec\"\n43\t },\n44\t {\n45\t \"id\": \"cogir\",\n46\t \"name\": \"Cogir Immobilier\",\n47\t \"url\": \"https://www.cogir.net\",\n48\t \"listing_url\": \"https://www.cogir.net/gestion-immeubles-residentiels.html\",\n49\t \"sectors\": \"Québec (Le Bacc, Loretteville, Père-Lelièvre...)\",\n50\t \"connector\": \"cogir\",\n51\t \"status\": \"actif\",\n52\t \"region\": \"Québec\"\n53\t },\n54\t {\n55\t \"id\": \"immostar\",\n56\t \"name\": \"Immostar\",\n57\t \"url\": \"https://immostar.ca\",\n58\t \"listing_url\": \"https://immostaralouer.ca\",\n59\t \"sectors\": \"Ste-Foy/blvd Laurier, Cap-Rouge, Lévis (Loges St-Nicolas), Wendake\",\n60\t \"connector\": \"immostar\",\n61\t \"status\": \"actif\",\n62\t \"region\": \"Québec\"\n63\t },\n64\t {\n65\t \"id\": \"dma_locago\",\n66\t \"name\": \"DMA / Locago (Douville, Moffet & Associés)\",\n67\t \"url\": \"https://locago.ca\",\n68\t \"listing_url\": \"https://locago.ca\",\n69\t \"sectors\": \"Ste-Foy, Vanier, Lebourgneuf, Charlesbourg, Beauport\",\n70\t \"connector\": \"dma_locago\",\n71\t \"status\": \"actif\",\n72\t \"region\": \"Québec\"\n73\t },\n74\t {\n75\t \"id\": \"groupe_dallaire\",\n76\t \"name\": \"Groupe Dallaire (Faubourg du Moulin)\",\n77\t \"url\": \"https://faubourgdumoulin.ca\",\n78\t \"listing_url\": \"https://faubourgdumoulin.ca/disponibilite\",\n79\t \"sectors\": \"Québec (Alizé I-II)\",\n80\t \"connector\": \"groupe_dallaire\",\n81\t \"status\": \"actif (dégradé) — le pare-feu de faubourgdumoulin.ca bloque l'IP du serveur Lou-Ka depuis ~2026-08-17 : repli automatique sur proxy Scrapfly (timeout direct 8 s)\",\n82\t \"region\": \"Québec\"\n83\t },\n84\t {\n85\t \"id\": \"trudel\",\n86\t \"name\": \"Trudel (Fleur de Lys, LGC)\",\n87\t \"url\": \"https://trudel.ca\",\n88\t \"listing_url\": \"https://18juillet.trudel.ca\",\n89\t \"sectors\": \"Vanier/Fleur de Lys, Charlesbourg\",\n90\t \"connector\": \"trudel\",\n91\t \"status\": \"suspendu — widget RealVuu (app.realvuu.com) hors-ligne depuis 2026-08-10 (DNS NXDOMAIN) ; connecteur désactivé (disabled=True) et annonces archivées le 2026-08-18\",\n92\t \"region\": \"Québec\"\n93\t },\n94\t {\n95\t \"id\": \"roussin\",\n96\t \"name\": \"Immeubles Roussin\",\n97\t \"url\": \"https://immeublesroussin.com\",\n98\t \"listing_url\": \"https://immeublesroussin.com\",\n99\t \"sectors\": \"Sainte-Foy, Lévis, Beauport\",\n100\t \"connector\": \"roussin\",\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"dc01cc28-1155-4dbc-96e0-65221ec9cf83","timestamp":"2026-08-24T21:45:12.376Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/ingest.py","usage":{"total_tokens":53956,"tool_uses":27,"duration_ms":28554},"last_tool_name":"Read","uuid":"7cf169e1-2fd7-4f7c-91a9-22c552ccf625","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJg67snHa9AQR8DVBVa","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016WPAepNxKSeHZCWQJ4wi6M","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1939,"cache_read_input_tokens":51920,"cache_creation":{"ephemeral_5m_input_tokens":1939,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4eacf2e3-159f-4084-8825-2f693a3e094b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016WPAepNxKSeHZCWQJ4wi6M","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# ingest.py : pipeline d'ingestion — exécute les connecteurs et synchronise\n5\t# la base (ajouts / mises à jour / retraits) = contenu toujours à jour\n6\t# -----------------------------------------------------------------------------\n7\tfrom __future__ import annotations\n8\t\n9\timport sys\n10\timport time\n11\timport traceback\n12\t\n13\tfrom . import db\n14\tfrom .connectors import CONNECTORS\n15\t\n16\t# Disjoncteur (circuit breaker) : une source qui a échoué aux BREAKER_FAILS\n17\t# derniers passages consécutifs est SAUTÉE (les timeouts/retries des sources\n18\t# mortes ralentissaient tout le cycle), puis retentée un passage sur\n19\t# BREAKER_RETRY_EVERY. Les sauts sont consignés dans sync_log (ok=0,\n20\t# message « disjoncteur: … ») — ils comptent donc comme échecs consécutifs,\n21\t# ce qui fait avancer le compteur jusqu'à la prochaine retentative.\n22\tBREAKER_FAILS = 5\n23\tBREAKER_RETRY_EVERY = 5\n24\t\n25\t\n26\tdef _consecutive_failures(con, source: str) -> int:\n27\t \"\"\"Nombre d'entrées sync_log ok=0 consécutives (les plus récentes).\"\"\"\n28\t rows = con.execute(\n29\t \"SELECT ok FROM sync_log WHERE source=? ORDER BY ts DESC LIMIT 60\",\n30\t (source,)).fetchall()\n31\t n = 0\n32\t for r in rows:\n33\t if r[\"ok\"]:\n34\t break\n35\t n += 1\n36\t return n\n37\t\n38\t\n39\tdef run(sources: list[str] | None = None) -> list[dict]:\n40\t \"\"\"Exécute l'ingestion pour toutes les sources (ou celles demandées).\"\"\"\n41\t con = db.connect()\n42\t results = []\n43\t explicit = sources is not None # sync ciblé : ignorer le disjoncteur\n44\t targets = sources or list(CONNECTORS.keys())\n45\t for sid in targets:\n46\t cls = CONNECTORS.get(sid)\n47\t if cls is None:\n48\t print(f\"[lou-ka] connecteur inconnu : {sid}\", file=sys.stderr)\n49\t continue\n50\t if not explicit:\n51\t fails = _consecutive_failures(con, sid)\n52\t if fails >= BREAKER_FAILS and fails % BREAKER_RETRY_EVERY != 0:\n53\t print(f\"[lou-ka] {sid} sauté (disjoncteur : \"\n54\t f\"{fails} échecs consécutifs)\")\n55\t db.log_failure(con, sid,\n56\t f\"disjoncteur: sauté ({fails} échecs consécutifs)\")\n57\t results.append({\"source\": sid, \"skipped\": True, \"fails\": fails})\n58\t continue\n59\t t0 = time.time()\n60\t print(f\"[lou-ka] sync {sid} ...\")\n61\t try:\n62\t listings = cls().fetch()\n63\t finalized, dropped = [], 0\n64\t for lst in listings:\n65\t try:\n66\t finalized.append(lst.finalize())\n67\t except Exception: # une annonce malformée ne bloque pas la source\n68\t dropped += 1\n69\t stats = db.sync_source(con, sid, finalized)\n70\t stats[\"seconds\"] = round(time.time() - t0, 1)\n71\t if dropped:\n72\t stats[\"dropped\"] = dropped\n73\t if stats.get(\"alert\"):\n74\t print(f\"[lou-ka] ⚠ ALERTE {sid} : {stats['alert']}\")\n75\t print(f\"[lou-ka] {stats}\")\n76\t results.append(stats)\n77\t except Exception as exc: # robustesse : une source ne bloque pas les autres\n78\t db.log_failure(con, sid, f\"{exc}\")\n79\t traceback.print_exc()\n80\t results.append({\"source\": sid, \"error\": str(exc)})\n81\t # statistiques du planificateur SQLite : sans elles, les requêtes bbox de\n82\t # la carte (Ka Maps) retombent sur idx_listings_active au lieu du géo-index\n83\t try:\n84\t con.execute(\"PRAGMA optimize\")\n85\t except Exception:\n86\t pass\n87\t con.close()\n88\t return results\n89\t\n90\t\n91\tdef watch(interval_seconds: int = 3600) -> None:\n92\t \"\"\"Boucle de rafraîchissement périodique (pseudo-webhook par sondage).\"\"\"\n93\t while True:\n94\t run()\n95\t try: # géocoder les nouvelles adresses EN LOT (Adresses Québec)\n96\t from . import geocode\n97\t geocode.run_batch()\n98\t except Exception as exc:\n99\t print(f\"[lou-ka] geocode: erreur non bloquante: {exc}\", file=sys.stderr)\n100\t try: # commodités de proximité des nouveaux immeubles (cache aussi)\n101\t from . import poi\n102\t poi.run(limit=80)\n103\t except Exception as exc:\n104\t print(f\"[lou-ka] poi: erreur non bloquante: {exc}\", file=sys.stderr)\n105\t try: # aire de diffusion (stats de quartier) des nouvelles annonces\n106\t from . import quartier\n107\t quartier.enrich()\n108\t except Exception as exc:\n109\t print(f\"[lou-ka] quartier: erreur non bloquante: {exc}\", file=sys.stderr)\n110\t try: # rafraîchir le score de complétude (le géocodage vient de compléter\n111\t # des coordonnées) + publication/quarantaine\n112\t from . import quality\n113\t quality.backfill()\n114\t except Exception as exc:\n115\t print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n116\t try: # contrôle qualité des images, par lots incrémentaux\n117\t from . import imgcheck\n118\t imgcheck.run(1500)\n119\t except Exception as exc:\n120\t print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n121\t try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n122\t from . import dedup\n123\t stats = dedup.run()\n124\t print(f\"[lou-ka] dedup: {stats}\")\n125\t except Exception as exc:\n126\t print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n127\t try: # juste valeur locative (fair value) — après dedup/geocode/quality\n128\t from . import fairvalue\n129\t fairvalue.compute_all()\n130\t except Exception as exc:\n131\t print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n132\t try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n133\t # incrémentaux des nouveaux immeubles — après géocodage\n134\t from . import environment, kascores\n135\t environment.run(limit=4)\n136\t stats_ks = kascores.run()\n137\t print(f\"[lou-ka] kascores: {stats_ks}\")\n138\t except Exception as exc:\n139\t print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n140\t try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n141\t from . import hydro\n142\t hydro.precompute(limit=100, budget=30)\n143\t except Exception as exc:\n144\t print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n145\t try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n146\t # puis backfill des adresses jamais vérifiées (coût borné)\n147\t from . import tal\n148\t tal.precompute(limit=60, budget=600)\n149\t except Exception as exc:\n150\t print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n151\t print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n152\t time.sleep(interval_seconds)\n153\t\n154\t\n155\tif __name__ == \"__main__\":\n156\t run(sys.argv[1:] or None)\n157\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5c4046c4-3fb8-45e7-95cf-a76449408092","timestamp":"2026-08-24T21:45:13.854Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/geocode.py","usage":{"total_tokens":56646,"tool_uses":28,"duration_ms":30674},"last_tool_name":"Read","uuid":"8a25ade3-2dc8-4240-9b06-c454b06a4239","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgCpCRwFshudPEdLYR","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011JxLZSKusBr8zEvjYGzrtx","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/geocode.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2685,"cache_read_input_tokens":53859,"cache_creation":{"ephemeral_5m_input_tokens":2685,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ad710d52-8b0e-443e-bcce-fd1d2e91b6ac"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011JxLZSKusBr8zEvjYGzrtx","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# geocode.py : géocodage des adresses via Nominatim (OpenStreetMap)\n5\t# - cache persistant (table geocode_cache) : une adresse n'est géocodée\n6\t# qu'une seule fois, et par immeuble (adresse normalisée), pas par annonce\n7\t# - politesse : 1 requête/seconde max, User-Agent identifiable\n8\t# - validation : les coordonnées doivent tomber dans la région attendue\n9\t# (bounding box par ville), sinon flag geocode_failed — jamais de\n10\t# coordonnées bidon\n11\t# -----------------------------------------------------------------------------\n12\tfrom __future__ import annotations\n13\t\n14\timport re\n15\timport time\n16\t\n17\timport requests\n18\t\n19\tfrom . import db\n20\tfrom .normalize import strip_accents\n21\t\n22\tNOMINATIM_URL = \"https://nominatim.openstreetmap.org/search\"\n23\t# Repli officiel du gouvernement du Québec (Adresses Québec / MERN) : couvre\n24\t# les rues trop récentes pour OpenStreetMap (développements neufs de Lévis…)\n25\tAQ_URL = (\"https://servicescarto.mern.gouv.qc.ca/pes/rest/services/Territoire/\"\n26\t \"Adresse_Geocodage/GeocodeServer/findAddressCandidates\")\n27\tAQ_MIN_SCORE = 75\n28\t# ASCII pur : le serveur ArcGIS d'Adresses Québec retourne 500 si l'en-tête\n29\t# User-Agent contient des accents (latin-1)\n30\tUSER_AGENT = \"LouKaBot/1.0 (agregateur logements Quebec; +contact@spboucher.ai)\"\n31\tREQUEST_DELAY = 1.1 # règle Nominatim : max 1 req/s\n32\tRETRY_FAILED_AFTER = 30 * 86400 # re-tenter les échecs après 30 jours\n33\t\n34\t# Bounding boxes (lat_min, lat_max, lng_min, lng_max)\n35\t_BBOX_QUEBEC = (46.55, 47.10, -71.75, -70.85) # Québec, Lévis, St-Augustin\n36\t# garde-fou provincial : couvre Gatineau, l'Abitibi, le Saguenay, la Côte-Nord\n37\t# et la Gaspésie (expansion provinciale) — même boîte que schema.finalize()\n38\t_BBOX_SUD_QC = (44.50, 63.00, -80.00, -56.00)\n39\t\n40\t_VILLES_QUEBEC = {\"quebec\", \"levis\", \"saint-augustin-de-desmaures\",\n41\t \"l'ancienne-lorette\", \"ancienne-lorette\"}\n42\t\n43\t\n44\tdef _bbox_for(city: str) -> tuple[float, float, float, float]:\n45\t key = strip_accents((city or \"\").strip().lower())\n46\t return _BBOX_QUEBEC if key in _VILLES_QUEBEC else _BBOX_SUD_QC\n47\t\n48\t\n49\tdef _in_bbox(lat: float, lng: float, bbox: tuple) -> bool:\n50\t return bbox[0] <= lat <= bbox[1] and bbox[2] <= lng <= bbox[3]\n51\t\n52\t\n53\tdef norm_key(address: str) -> str:\n54\t \"\"\"Clé de cache : adresse normalisée (casse, accents, espaces, n° d'app.).\"\"\"\n55\t s = strip_accents((address or \"\").lower())\n56\t s = re.sub(r\"\\b(?:app?t?|appartement|unite|suite|#)\\.?\\s*[\\w-]+\\b\", \" \", s)\n57\t s = re.sub(r\"[^a-z0-9]+\", \" \", s)\n58\t return re.sub(r\"\\s+\", \" \", s).strip()\n59\t\n60\t\n61\tclass Geocoder:\n62\t def __init__(self, con) -> None:\n63\t self.con = con\n64\t self.session = requests.Session()\n65\t self.session.headers[\"User-Agent\"] = USER_AGENT\n66\t self._last = 0.0\n67\t\n68\t @staticmethod\n69\t def _clean(address: str) -> str:\n70\t \"\"\"Nettoyages qui aident Nominatim sur les adresses québécoises.\"\"\"\n71\t s = address.strip()\n72\t # « 14340, rue X » (virgule après le n° civique, style français) :\n73\t # la retirer — sinon le lot Adresses Québec coupe au premier « , »\n74\t # et n'envoie que « 14340 » (échec systématique)\n75\t s = re.sub(r\"^(\\d+[A-Za-z]?),\\s+\", r\"\\1 \", s)\n76\t # « 101-2905 Rue X » = unité 101, civique 2905 -> garder le civique\n77\t s = re.sub(r\"^(\\d+)-(\\d+)\\s\", r\"\\2 \", s)\n78\t # « 6275 et 6375 boulevard X » -> premier civique\n79\t s = re.sub(r\"^(\\d+)\\s+et\\s+\\d+\\s\", r\"\\1 \", s)\n80\t # « Montréal - Laval » / « Montréal - Île-des-Soeurs » -> garder le vrai lieu\n81\t s = re.sub(r\"montr[ée]al\\s*-\\s*\", \"\", s, flags=re.I)\n82\t # « bureau 105 » / « suite 3 » / « local B » : suffixes de bureau\n83\t s = re.sub(r\",?\\s*(?:bureau|suite|local|app\\.?|apt\\.?)\\s*[\\w-]+\\b\", \"\", s, flags=re.I)\n84\t # « Vanier (Québec) » -> « Vanier, Québec »\n85\t s = re.sub(r\"\\s*\\((qu[ée]bec)\\)\", r\", \\1\", s, flags=re.I)\n86\t # abréviations cardinales : « Rue Salaberry O » -> « Ouest »\n87\t s = re.sub(r\"\\bO\\.?(?=,|\\s*$)\", \"Ouest\", s)\n88\t s = re.sub(r\"\\bE\\.?(?=,|\\s*$)\", \"Est\", s)\n89\t return s\n90\t\n91\t def _build_query(self, address: str, city: str) -> str:\n92\t q = self._clean(address)\n93\t key = strip_accents(q.lower())\n94\t if city and strip_accents(city.lower()) not in key:\n95\t q += f\", {city}\"\n96\t if \"quebec\" not in strip_accents(q.lower()) and \"qc\" not in q.lower():\n97\t q += \", Québec\"\n98\t return q + \", Canada\"\n99\t\n100\t def _query_nominatim(self, params: dict) -> tuple[float, float] | None:\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"46e9b7c6-e48a-459c-b6cc-81c1d76a7a9c","timestamp":"2026-08-24T21:45:15.974Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/poi.py","usage":{"total_tokens":56649,"tool_uses":29,"duration_ms":30987},"last_tool_name":"Read","uuid":"14cfc101-6d09-4317-b20b-09453888436f","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgCpCRwFshudPEdLYR","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Ly3hJi69SGhxDgKYfsxopj","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/poi.py","limit":80},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2685,"cache_read_input_tokens":53859,"cache_creation":{"ephemeral_5m_input_tokens":2685,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ca632d1b-ffd1-4360-bfdb-6e91b443a1de"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Ly3hJi69SGhxDgKYfsxopj","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# poi.py : commodités de proximité par immeuble via Overpass (OpenStreetMap)\n5\t# Pour chaque immeuble géolocalisé, une requête Overpass unique récupère\n6\t# les points d'intérêt utiles à un locataire (épicerie, pharmacie, école,\n7\t# garderie, parc, arrêt de bus, gym, clinique…) ; on retient le PLUS PROCHE\n8\t# de chaque catégorie avec sa distance. Cache permanent par coordonnées\n9\t# (table poi_cache, clé arrondie à 4 décimales ≈ 11 m : les unités d'un\n10\t# même immeuble partagent la même entrée). Politesse : 1 requête/seconde.\n11\t# -----------------------------------------------------------------------------\n12\tfrom __future__ import annotations\n13\t\n14\timport json\n15\timport math\n16\timport time\n17\t\n18\timport requests\n19\t\n20\tfrom . import db\n21\t\n22\t# Miroirs Overpass (rotation en cas d'erreur/limitation) — kumi.systems\n23\t# tolère mieux les gros volumes que l'instance officielle\n24\tOVERPASS_URLS = [\n25\t \"https://overpass.kumi.systems/api/interpreter\",\n26\t \"https://overpass-api.de/api/interpreter\",\n27\t]\n28\tUSER_AGENT = \"LouKaBot/1.0 (agregateur logements Quebec; +contact@spboucher.ai)\"\n29\tREQUEST_DELAY = 2.0\n30\tREFRESH_AFTER = 90 * 86400 # les POI bougent peu : rafraîchir aux ~3 mois\n31\t\n32\t# Stratégie « par tuiles » : plutôt qu'une requête par immeuble (l'union\n33\t# d'around() est très coûteuse côté Overpass), on télécharge TOUS les POI\n34\t# des catégories par tuile de 0,5° couvrant nos immeubles (~10 tuiles pour\n35\t# Québec/Lévis + Grand Montréal), puis on calcule les plus proches en local.\n36\tTILE = 0.5\n37\tTILE_MARGIN = 0.04 # ~4 km > plus grand rayon de catégorie (3 km)\n38\t\n39\t# Catégories : (clé, libellé FR, sélecteur Overpass, rayon m)\n40\tCATEGORIES: list[tuple[str, str, str, int]] = [\n41\t (\"epicerie\", \"Épicerie\", '[\"shop\"=\"supermarket\"]', 1500),\n42\t (\"depanneur\", \"Dépanneur\", '[\"shop\"=\"convenience\"]', 800),\n43\t (\"pharmacie\", \"Pharmacie\", '[\"amenity\"=\"pharmacy\"]', 1500),\n44\t (\"ecole\", \"École\", '[\"amenity\"=\"school\"]', 1500),\n45\t (\"garderie\", \"Garderie\", '[\"amenity\"~\"^(kindergarten|childcare)$\"]', 1500),\n46\t (\"parc\", \"Parc\", '[\"leisure\"=\"park\"]', 1200),\n47\t (\"bus\", \"Arrêt de bus\", '[\"highway\"=\"bus_stop\"]', 600),\n48\t (\"metro\", \"Métro\", '[\"railway\"=\"station\"][\"station\"=\"subway\"]', 1500),\n49\t (\"gym\", \"Gym\", '[\"leisure\"=\"fitness_centre\"]', 1500),\n50\t (\"cafe\", \"Café\", '[\"amenity\"=\"cafe\"]', 1000),\n51\t (\"clinique\", \"Clinique / CLSC\", '[\"amenity\"~\"^(clinic|doctors)$\"]', 2000),\n52\t (\"hopital\", \"Hôpital\", '[\"amenity\"=\"hospital\"]', 3000),\n53\t (\"bibliotheque\", \"Bibliothèque\", '[\"amenity\"=\"library\"]', 2000),\n54\t]\n55\t\n56\tLABELS = {cat: label for cat, label, _, _ in CATEGORIES}\n57\t\n58\t\n59\tdef coord_key(lat: float, lng: float) -> str:\n60\t return f\"{round(lat, 4)},{round(lng, 4)}\"\n61\t\n62\t\n63\tdef _haversine_m(lat1: float, lng1: float, lat2: float, lng2: float) -> float:\n64\t r = 6371000.0\n65\t p1, p2 = math.radians(lat1), math.radians(lat2)\n66\t dp, dl = math.radians(lat2 - lat1), math.radians(lng2 - lng1)\n67\t a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2\n68\t return 2 * r * math.asin(math.sqrt(a))\n69\t\n70\t\n71\tdef _tile_of(lat: float, lng: float) -> tuple[int, int]:\n72\t return (math.floor(lat / TILE), math.floor(lng / TILE))\n73\t\n74\t\n75\tdef _build_tile_query(ty: int, tx: int) -> str:\n76\t \"\"\"Tous les POI des catégories dans la tuile (bbox élargie de la marge).\"\"\"\n77\t s = ty * TILE - TILE_MARGIN\n78\t n = (ty + 1) * TILE + TILE_MARGIN\n79\t w = tx * TILE - TILE_MARGIN\n80\t e = (tx + 1) * TILE + TILE_MARGIN\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a4561f27-8057-4cf1-83a0-436e9eed6022","timestamp":"2026-08-24T21:45:16.288Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/dedup.py","usage":{"total_tokens":56652,"tool_uses":30,"duration_ms":31009},"last_tool_name":"Read","uuid":"cdcc930a-2b20-4b05-af1f-c1df8ddeff93","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgCpCRwFshudPEdLYR","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019wGSzmNZ3Wu2V4EfaWJELV","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/dedup.py","limit":80},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2685,"cache_read_input_tokens":53859,"cache_creation":{"ephemeral_5m_input_tokens":2685,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3ad0d8f3-e861-4972-bd74-72ae15ec7b6b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019wGSzmNZ3Wu2V4EfaWJELV","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# dedup.py : déduplication INTER-SOURCES — une même unité publiée sur plusieurs\n5\t# plateformes (ex. un gestionnaire agrégé en direct ET via LogisQuébec,\n6\t# ou un particulier sur Kijiji ET Facebook) ne doit apparaître qu'une\n7\t# fois côté site.\n8\t#\n9\t# Principe (recalculé intégralement à chaque passe, idempotent) :\n10\t# 1. Blocage : regrouper les annonces actives par proximité (grille lat/lng\n11\t# ~110 m + cases voisines) OU par clé d'adresse normalisée (n° civique +\n12\t# rue sans type de voie + ville).\n13\t# 2. Confirmation d'une paire : même type d'unité (si connu des deux côtés),\n14\t# prix à ±4 % (si connu), et signal géographique/adresse concordant.\n15\t# 3. Résolution : le membre de la source la PLUS autoritaire devient canonique\n16\t# (dup_of=NULL) ; les autres pointent vers lui (dup_of=uid canonique). Le\n17\t# canonique liste les autres plateformes dans dup_sources (badge « aussi\n18\t# publiée sur… »). Rien n'est supprimé.\n19\t#\n20\t# Le web (web.py) n'affiche que les annonces canoniques : `WHERE dup_of IS NULL`.\n21\t# Appelé par ingest.watch() APRÈS le géocodage (les coordonnées sont l'ingrédient\n22\t# principal du blocage).\n23\t# -----------------------------------------------------------------------------\n24\tfrom __future__ import annotations\n25\t\n26\timport json\n27\t\n28\tfrom . import db\n29\tfrom .normalize import strip_accents\n30\t\n31\t# Autorité des sources : plus l'indice est BAS, plus la source est prioritaire\n32\t# comme canonique. 0 = gestionnaire/courtier en direct (donnée d'origine),\n33\t# 10 = portail agrégateur, 20 = petites annonces / particuliers.\n34\tPORTALS = {\"logisquebec\", \"louer_ca\", \"rentals_ca\", \"zumper\", \"padmapper\",\n35\t \"realtor\", \"mercini\"}\n36\tCLASSIFIEDS = {\"kijiji\", \"lespac\", \"fb_marketplace\", \"bons_locataires\",\n37\t \"roomies\", \"duproprio\", \"oklouer\"}\n38\t\n39\t\n40\tdef _authority(source: str) -> int:\n41\t if source in CLASSIFIEDS:\n42\t return 20\n43\t if source in PORTALS:\n44\t return 10\n45\t return 0 # tout connecteur de gestionnaire/courtier en direct\n46\t\n47\t\n48\t# types de voie retirés pour comparer les rues (fr + en)\n49\t_STREET_TYPES = {\n50\t \"rue\", \"avenue\", \"av\", \"ave\", \"boulevard\", \"boul\", \"bd\", \"chemin\", \"ch\",\n51\t \"montee\", \"montée\", \"rang\", \"place\", \"pl\", \"cote\", \"côte\", \"impasse\",\n52\t \"terrasse\", \"tsse\", \"allee\", \"allée\", \"croissant\", \"crois\", \"cours\",\n53\t \"street\", \"st\", \"road\", \"rd\", \"drive\", \"dr\", \"lane\", \"ln\", \"court\", \"crt\",\n54\t \"way\", \"circle\", \"blvd\", \"de\", \"du\", \"des\", \"la\", \"le\", \"les\", \"l\", \"d\",\n55\t}\n56\t\n57\t\n58\t_UNIT_MARKERS = {\"app\", \"apt\", \"appartement\", \"unit\", \"unite\", \"suite\", \"bureau\"}\n59\t# points cardinaux unifiés FR/EN (« Sherbrooke Ouest » == « Sherbrooke West »)\n60\t_DIRECTIONS = {\"ouest\": \"o\", \"west\": \"o\", \"w\": \"o\", \"est\": \"e\", \"east\": \"e\",\n61\t \"e\": \"e\", \"nord\": \"n\", \"north\": \"n\", \"n\": \"n\", \"sud\": \"s\",\n62\t \"south\": \"s\", \"s\": \"s\"}\n63\t# marqueurs de fin de rue : province/pays (PAS les noms de ville — « Sherbrooke »\n64\t# est aussi un nom de rue fréquent ; le nom de la ville de l'annonce est géré\n65\t# séparément)\n66\t_ADDR_STOP = {\"qc\", \"quebec\", \"canada\", \"que\", \"ca\"}\n67\t\n68\t\n69\tdef _parse_address(address: str, city: str) -> tuple[str | None, str | None]:\n70\t \"\"\"Retourne (clé d'adresse, numéro d'unité) à partir d'une adresse civique.\n71\t\n72\t - clé = « civique|rue-significative|ville » sans accents ni type de voie,\n73\t points cardinaux unifiés FR/EN ; None si aucun numéro civique fiable.\n74\t - numéro d'unité (app/apt/#/suite ou nombre surnuméraire en tête) : sert à\n75\t NE PAS fusionner deux logements distincts au même immeuble. None si absent.\n76\t\n77\t Gère « 304 4557 Rue Sherbrooke » (304 = unité, 4557 = civique), « 12100,\n78\t rue Rodolphe-Forget » (virgule après le civique) et « 4557 Sherbrooke O ».\n79\t \"\"\"\n80\t a = strip_accents((address or \"\").lower())\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"99a237e8-1161-419e-bc85-013c05b4c24c","timestamp":"2026-08-24T21:45:16.308Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/hydro.py","usage":{"total_tokens":62289,"tool_uses":31,"duration_ms":33336},"last_tool_name":"Read","uuid":"10a2d3eb-975c-4baa-8268-aa91b9069e0b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgPHzAGMgxuYmitmze","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Tz1aRJWiy5o6R5MSjVhWyy","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/hydro.py","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":5631,"cache_read_input_tokens":56544,"cache_creation":{"ephemeral_5m_input_tokens":5631,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c8e5a6cd-5d7d-46e6-8379-6004a1ed4420"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Tz1aRJWiy5o6R5MSjVhWyy","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# hydro.py : estimation du coût d'électricité à une adresse — Hydro-Québec\n5\t#\n6\t# Reproduit le parcours PUBLIC de l'outil « Estimer les coûts d'électricité\n7\t# à une résidence » (session.hydroquebec.com), rétro-conçu depuis son SPA :\n8\t# 1. POST {clWebServicePublicApiURL}public/api/v3_0/lieu-consommation\n9\t# body {noCivique, codePostal, recaptchaResponse} -> liste de lieux\n10\t# (idConso, cleUnique, adresse) ;\n11\t# 2. POST {drcvPublicApiURL}public/api/v3_0/consommation/estimation\n12\t# body {cleUnique, idConso} -> coût annuel estimé.\n13\t# Les deux étapes exigent un jeton reCAPTCHA v2 valide\n14\t# (site key 6Lf08Q0UAAAAABCA7z47p2tMxa5_fY0wmn8DDPsu). Il est obtenu via un\n15\t# solveur enfichable (2captcha / CapSolver) ET les requêtes sortent par le\n16\t# proxy résidentiel canadien Bright Data (contourne le géoblocage / anti-bot).\n17\t#\n18\t# Configuration (.env) :\n19\t# HQ_CAPTCHA_KEY clé du solveur (obligatoire pour l'appel live)\n20\t# HQ_CAPTCHA_PROVIDER \"2captcha\" (défaut) | \"capsolver\"\n21\t# BRIGHTDATA_* / OXYLABS_* : proxy résidentiel (réutilise la pile projet)\n22\t#\n23\t# Sans HQ_CAPTCHA_KEY : estimate() renvoie {\"disponible\": False, ...} et le\n24\t# bloc fiche reste masqué — jamais d'estimation inventée.\n25\t# -----------------------------------------------------------------------------\n26\tfrom __future__ import annotations\n27\t\n28\timport json\n29\timport os\n30\timport re\n31\timport sqlite3\n32\timport time\n33\timport urllib.parse\n34\timport urllib.request\n35\tfrom pathlib import Path\n36\t\n37\tDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"hydro.db\"\n38\tTTL = 180 * 86400 # succès : la conso bouge lentement\n39\tNEG_TTL = 30 * 86400 # échec (adresse inconnue) : re-tenter dans 30 j\n40\t # — borne le coût captcha des cycles watch\n41\t\n42\tSITE_KEY = \"6Lf08Q0UAAAAABCA7z47p2tMxa5_fY0wmn8DDPsu\"\n43\tPAGE_URL = (\"https://session.hydroquebec.com/portail/fr/web/clientele/\"\n44\t \"estimation-consommation\")\n45\tURL_LIEU = (\"https://services-cl.solutions.hydroquebec.com/wsapi/webpublic/\"\n46\t \"public/api/v3_0/lieu-consommation\")\n47\tURL_ESTIM = (\"https://services-cl.solutions.hydroquebec.com/conso/webpublic/\"\n48\t \"public/api/v3_0/consommation/estimation\")\n49\tUA = (\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \"\n50\t \"(KHTML, like Gecko) Chrome/126.0 Safari/537.36\")\n51\t\n52\t\n53\t# --- proxy résidentiel (Bright Data Web Unlocker en repli d'Oxylabs) ----------\n54\tdef _opener() -> urllib.request.OpenerDirector:\n55\t proxy = None\n56\t if os.environ.get(\"OXYLABS_PROXY_USER\"):\n57\t u = os.environ[\"OXYLABS_PROXY_USER\"]\n58\t p = os.environ[\"OXYLABS_PROXY_PASS\"]\n59\t host = os.environ.get(\"OXYLABS_PROXY\", \"pr.oxylabs.io:7777\")\n60\t proxy = f\"http://{u}-cc-CA:{p}@{host}\"\n61\t if proxy:\n62\t h = urllib.request.ProxyHandler({\"http\": proxy, \"https\": proxy})\n63\t return urllib.request.build_opener(h)\n64\t return urllib.request.build_opener()\n65\t\n66\t\n67\t# --- solveur reCAPTCHA v2 enfichable ------------------------------------------\n68\tdef _solve_captcha() -> str | None:\n69\t key = os.environ.get(\"HQ_CAPTCHA_KEY\")\n70\t if not key:\n71\t return None\n72\t provider = os.environ.get(\"HQ_CAPTCHA_PROVIDER\", \"2captcha\").lower()\n73\t if provider == \"capsolver\":\n74\t return _capsolver(key)\n75\t return _twocaptcha(key)\n76\t\n77\t\n78\tdef _twocaptcha(key: str) -> str | None:\n79\t r = urllib.request.urlopen(urllib.request.Request(\n80\t \"https://2captcha.com/in.php\",\n81\t data=urllib.parse.urlencode({\n82\t \"key\": key, \"method\": \"userrecaptcha\", \"googlekey\": SITE_KEY,\n83\t \"pageurl\": PAGE_URL, \"json\": 1}).encode()), timeout=30)\n84\t got = json.load(r)\n85\t if got.get(\"status\") != 1:\n86\t return None\n87\t cid = got[\"request\"]\n88\t for _ in range(24):\n89\t time.sleep(5)\n90\t res = json.load(urllib.request.urlopen(\n91\t f\"https://2captcha.com/res.php?key={key}&action=get&id={cid}&json=1\",\n92\t timeout=30))\n93\t if res.get(\"status\") == 1:\n94\t return res[\"request\"]\n95\t if res.get(\"request\") != \"CAPCHA_NOT_READY\":\n96\t return None\n97\t return None\n98\t\n99\t\n100\tdef _capsolver(key: str) -> str | None:\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ace2b06d-8804-4997-8cfc-f1c078b06b13","timestamp":"2026-08-24T21:45:18.641Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/pdfgen.py","usage":{"total_tokens":62291,"tool_uses":32,"duration_ms":33755},"last_tool_name":"Read","uuid":"6e1edcd4-fbed-4c4e-b046-07ae286029ec","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgPHzAGMgxuYmitmze","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HiJbDAoCVRHmiPZKrfEJcm","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","limit":80},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":5631,"cache_read_input_tokens":56544,"cache_creation":{"ephemeral_5m_input_tokens":5631,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6c076aae-1caf-4a4a-b97c-1d4b63a66b5c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HiJbDAoCVRHmiPZKrfEJcm","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n5\t# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n6\t# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n7\t# chips, description structurée, inclusions, registre des loyers,\n8\t# historique TAL, inondation, qualité de l'air, coût d'électricité,\n9\t# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n10\t# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n11\t# des loyers, types, villes, gestionnaires)\n12\t# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n13\t# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n14\t# par la mise en page — aucune dépendance de police externe.\n15\t# -----------------------------------------------------------------------------\n16\tfrom __future__ import annotations\n17\t\n18\timport io\n19\timport json\n20\timport math\n21\timport time\n22\t\n23\timport qrcode\n24\timport requests\n25\tfrom reportlab.lib.colors import HexColor\n26\tfrom reportlab.lib.pagesizes import letter\n27\tfrom reportlab.lib.utils import ImageReader\n28\tfrom reportlab.pdfgen import canvas as rl_canvas\n29\t\n30\tfrom . import db\n31\t\n32\t# --- palette Lou-Ka (identité officielle) ------------------------------------\n33\tPAPER = HexColor(\"#ffffff\")\n34\tSURFACE = HexColor(\"#ffffff\")\n35\tINK = HexColor(\"#0b1330\") # bleu marine profond\n36\tINK2 = HexColor(\"#3e4560\")\n37\tINK3 = HexColor(\"#8a90a5\")\n38\tGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\n39\tGREEN_DEEP = HexColor(\"#0b1330\")\n40\tLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\n41\tLIME_SOFT = HexColor(\"#fff1e6\")\n42\tAMBER = HexColor(\"#ff6a00\")\n43\tAMBER_SOFT = HexColor(\"#fff1e6\")\n44\t\n45\tPAGE_W, PAGE_H = letter\n46\tM = 40 # marge\n47\tIMG_TIMEOUT = 6\n48\tUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n49\t\n50\tPOI_LABELS = {\n51\t \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n52\t \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n53\t \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n54\t \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n55\t}\n56\tPROX_LABELS = [\n57\t (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n58\t (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n59\t (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n60\t]\n61\t\n62\tNBSP = \" \"\n63\t\n64\t\n65\tdef _fmt_money(v) -> str:\n66\t return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n67\t\n68\t\n69\tdef _fmt_dist(m_: float) -> str:\n70\t return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n71\t else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n72\t\n73\t\n74\tdef _marche(m_: float) -> str:\n75\t return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n76\t\n77\t\n78\tdef _fetch_image(url: str) -> ImageReader | None:\n79\t try:\n80\t r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9cbb0dc7-dd31-4f8c-a9dc-7cd9ab883c35","timestamp":"2026-08-24T21:45:19.055Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/imgcheck.py","usage":{"total_tokens":62293,"tool_uses":33,"duration_ms":33956},"last_tool_name":"Read","uuid":"64332669-00f7-434c-b11b-b32d0453e5ad","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgPHzAGMgxuYmitmze","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019bYHS1w9KmV2smoBY4VAHb","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/imgcheck.py","limit":80},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":5631,"cache_read_input_tokens":56544,"cache_creation":{"ephemeral_5m_input_tokens":5631,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"296b12fe-7e45-42e4-9455-2a896470d478"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019bYHS1w9KmV2smoBY4VAHb","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# imgcheck.py : contrôle qualité des images d'annonces\n5\t# - liens morts (404, timeout, non-image) -> retirés\n6\t# - images minuscules / pixellisées -> retirées\n7\t# - placeholders de la source (même image générique\n8\t# répétée sur une grande part des annonces) -> retirés\n9\t# - doublons dans une même galerie (hash contenu) -> dédupliqués\n10\t# Résultat par annonce : listings.images_ok (galerie nettoyée, ordre\n11\t# d'origine conservé) + listings.img_audit (JSON de traçabilité).\n12\t# Les vérifications par URL sont mises en cache dans image_checks.\n13\t# -----------------------------------------------------------------------------\n14\tfrom __future__ import annotations\n15\t\n16\timport io\n17\timport json\n18\timport hashlib\n19\timport threading\n20\timport time\n21\tfrom concurrent.futures import ThreadPoolExecutor\n22\tfrom urllib.parse import urlsplit\n23\t\n24\timport requests\n25\t\n26\t# seuils de qualité\n27\tMIN_WIDTH = 250\n28\tMIN_HEIGHT = 180\n29\tMIN_BYTES = 6_000 # en dessous : icône / pixel de tracking\n30\tMAX_DOWNLOAD = 6_000_000 # on ne lit jamais plus de 6 Mo par image\n31\tTIMEOUT = 12\n32\tRECHECK_FAILED_DAYS = 7 # une image en échec est retentée après 7 jours\n33\t\n34\t# placeholder : même contenu présent sur >= PLACEHOLDER_MIN annonces distinctes,\n35\t# >= PLACEHOLDER_RATIO des annonces actives de la source ET réparti sur\n36\t# >= PLACEHOLDER_ADDRS adresses différentes. (Les photos d'immeuble ou de\n37\t# projet partagées entre les unités d'une MÊME adresse — piscine, gym,\n38\t# façade — sont légitimes : c'est la dispersion multi-adresses qui trahit\n39\t# le visuel générique « image non disponible ».)\n40\tPLACEHOLDER_MIN = 12\n41\tPLACEHOLDER_RATIO = 0.30\n42\tPLACEHOLDER_ADDRS = 8\n43\t\n44\t_UA = (\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \"\n45\t \"(KHTML, like Gecko) Chrome/126 Safari/537.36 LouKaImgBot/1.0 \"\n46\t \"(+https://www.lou-ka.com/bot; contact@spboucher.ai)\")\n47\t\n48\t# politesse : au plus 3 téléchargements simultanés PAR HÔTE (24 threads sur un\n49\t# même CDN = refus/limitation, et des images vivantes classées mortes à tort)\n50\t_HOST_SEMS: dict[str, threading.Semaphore] = {}\n51\t_HOST_LOCK = threading.Lock()\n52\t\n53\t\n54\tdef _host_sem(url: str) -> threading.Semaphore:\n55\t host = urlsplit(url).netloc\n56\t with _HOST_LOCK:\n57\t if host not in _HOST_SEMS:\n58\t _HOST_SEMS[host] = threading.Semaphore(3)\n59\t return _HOST_SEMS[host]\n60\t\n61\t\n62\tdef _check_url(url: str) -> dict:\n63\t \"\"\"Télécharge et inspecte une image. Retourne {status,width,height,bytes,sha1}.\"\"\"\n64\t out = {\"url\": url, \"status\": \"dead\", \"width\": None, \"height\": None,\n65\t \"bytes\": None, \"sha1\": None}\n66\t if not url or not url.startswith((\"http://\", \"https://\")):\n67\t return out\n68\t try:\n69\t with _host_sem(url):\n70\t resp = requests.get(url, timeout=TIMEOUT, stream=True,\n71\t headers={\"User-Agent\": _UA,\n72\t \"Accept\": \"image/*,*/*;q=0.5\"})\n73\t if resp.status_code in (404, 410):\n74\t return out # lien mort, définitif\n75\t if resp.status_code >= 400:\n76\t out[\"status\"] = \"error\" # 403/429/5xx : indéterminé, à retenter\n77\t return out\n78\t buf = io.BytesIO()\n79\t for chunk in resp.iter_content(65536):\n80\t buf.write(chunk)\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f24a7fcf-0072-497d-8997-2efed0c2f29c","timestamp":"2026-08-24T21:45:19.255Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/lou…","usage":{"total_tokens":67161,"tool_uses":34,"duration_ms":36057},"last_tool_name":"Bash","uuid":"1658b4f8-3b14-45cd-a850-11bc234e6d08","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgbnpHotCpHWvivfPo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012HEKHSzbbtP6Rzy97SR3ke","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/louka/shortterm"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4864,"cache_read_input_tokens":62175,"cache_creation":{"ephemeral_5m_input_tokens":4864,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5dcd3315-220f-47fb-a21e-f562bb7d3dc8"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012HEKHSzbbtP6Rzy97SR3ke","type":"tool_result","content":"total 80\ndrwxr-xr-x@ 9 simon-pierreboucher staff 288 Aug 23 09:31 .\ndrwxr-xr-x 41 simon-pierreboucher staff 1312 Aug 24 17:09 ..\n-rw-r--r--@ 1 simon-pierreboucher staff 592 Aug 22 19:34 __init__.py\ndrwxr-xr-x@ 7 simon-pierreboucher staff 224 Aug 23 09:35 __pycache__\ndrwxr-xr-x@ 34 simon-pierreboucher staff 1088 Aug 23 00:34 connectors\n-rw-r--r--@ 1 simon-pierreboucher staff 8109 Aug 22 19:35 db.py\n-rw-r--r--@ 1 simon-pierreboucher staff 3013 Aug 22 19:35 ingest.py\n-rw-r--r--@ 1 simon-pierreboucher staff 8340 Aug 22 19:35 schema.py\n-rw-r--r--@ 1 simon-pierreboucher staff 9073 Aug 23 09:31 web.py","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a4a6ad04-5896-4227-96cf-6e113ae89358","timestamp":"2026-08-24T21:45:21.383Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/fro…","usage":{"total_tokens":67163,"tool_uses":35,"duration_ms":36096},"last_tool_name":"Bash","uuid":"6a7c4a13-bc40-40ec-877f-59572568d425","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgbnpHotCpHWvivfPo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016v6QYB48KhwyRH87nm1eRi","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4864,"cache_read_input_tokens":62175,"cache_creation":{"ephemeral_5m_input_tokens":4864,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"98d6efdb-5c5e-4103-bbcb-8a62d3703e52"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016v6QYB48KhwyRH87nm1eRi","type":"tool_result","content":"total 416\ndrwxr-xr-x 22 simon-pierreboucher staff 704 Aug 24 16:23 .\ndrwxr-xr-x 15 simon-pierreboucher staff 480 Aug 24 16:47 ..\n-rw-r--r-- 1 simon-pierreboucher staff 3562 Aug 13 00:41 Bienvenue.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2653 Aug 8 22:56 Bot.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2384 Aug 17 20:25 Contact.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 18810 Aug 22 19:45 CourtTerme.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 10058 Aug 22 19:46 CourtTermeFiche.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2271 Aug 13 00:41 Favoris.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 7900 Aug 13 00:41 Gestion.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 4141 Aug 13 00:35 GestionPublic.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 25885 Aug 19 00:36 Home.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 4406 Aug 18 17:18 JusteValeur.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 8567 Aug 18 18:37 KaScores.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 24793 Aug 24 16:23 Listing.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3942 Aug 8 22:46 Passerelle.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3425 Aug 13 00:16 Privacy.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 18653 Aug 13 01:54 Profile.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 6770 Aug 17 20:25 PublicProfile.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 5739 Aug 23 09:32 Sources.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 10671 Aug 24 00:24 Stats.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3461 Aug 13 00:15 Terms.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 5075 Aug 13 03:56 Ville.tsx","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a887fe59-50a5-4056-a7f8-9e0911a3cc5a","timestamp":"2026-08-24T21:45:21.409Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading frontend/src/pages/Listing.tsx","usage":{"total_tokens":68624,"tool_uses":36,"duration_ms":37931},"last_tool_name":"Read","uuid":"fb973998-1547-4443-836a-8bb89622780c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgknoegJiFLn3W3RhT","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01La8g3vJa8oVcjU2AHCP8Vh","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","limit":150},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1457,"cache_read_input_tokens":67039,"cache_creation":{"ephemeral_5m_input_tokens":1457,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d8022c1a-06d8-4062-8db7-da75214533f0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01La8g3vJa8oVcjU2AHCP8Vh","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n5\t// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n6\t// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n7\t// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n8\t// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n9\t// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n10\t// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n11\t// blocs sans `order` passaient devant la galerie sur mobile).\n12\t// -----------------------------------------------------------------------------\n13\timport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\n14\timport { Link, useParams } from \"react-router-dom\";\n15\timport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\n16\timport QuartierBlock from \"../components/QuartierBlock\";\n17\timport SmartImg from \"../components/SmartImg\";\n18\timport FairValueBadge from \"../components/FairValueBadge\";\n19\timport PriceAnalysis from \"../components/PriceAnalysis\";\n20\timport HistoriqueTAL from \"../components/HistoriqueTAL\";\n21\timport RegistreLoyers from \"../components/RegistreLoyers\";\n22\timport RisqueInondation from \"../components/RisqueInondation\";\n23\timport QualiteAir from \"../components/QualiteAir\";\n24\timport EssenceProche from \"../components/EssenceProche\";\n25\timport HydroEstimation from \"../components/HydroEstimation\";\n26\timport CommercesProches from \"../components/CommercesProches\";\n27\timport { IcoAlert, IcoDoc } from \"../components/Icons\";\n28\timport KaScoresBlock from \"../components/KaScoresBlock\";\n29\timport { markSeen } from \"../search/seen\";\n30\t\n31\t// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\n32\tconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n33\t\n34\t// Icônes et libellés des commodités de proximité (louka/poi.py)\n35\tconst POI_META: Record = {\n36\t epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n37\t depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n38\t pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n39\t ecole: { icon: \"🏫\", label: \"École\" },\n40\t garderie: { icon: \"🧸\", label: \"Garderie\" },\n41\t parc: { icon: \"🌳\", label: \"Parc\" },\n42\t bus: { icon: \"🚌\", label: \"Bus\" },\n43\t metro: { icon: \"🚇\", label: \"Métro\" },\n44\t gym: { icon: \"🏋️\", label: \"Gym\" },\n45\t cafe: { icon: \"☕\", label: \"Café\" },\n46\t clinique: { icon: \"🩺\", label: \"Clinique\" },\n47\t hopital: { icon: \"🏥\", label: \"Hôpital\" },\n48\t bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n49\t};\n50\t\n51\t// Regroupement des POI en catégories repliables\n52\tconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n53\t { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n54\t { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n55\t { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n56\t { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n57\t { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n58\t];\n59\t\n60\t// Badge « prix vs marché » — seuils configurables\n61\tconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n62\t\n63\tconst PETS_LABEL: Record = {\n64\t oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n65\t};\n66\t\n67\tconst NBSP = \" \";\n68\t\n69\t/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\n70\tconst fmtMarche = (m: number): string =>\n71\t `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n72\t\n73\tfunction badgeMarche(price: number | null | undefined,\n74\t loyerSecteur: number | null | undefined) {\n75\t if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n76\t const delta = (price - loyerSecteur) / loyerSecteur;\n77\t const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n78\t if (delta <= SEUILS_MARCHE.bonDeal)\n79\t return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n80\t if (delta <= SEUILS_MARCHE.dansLeMarche)\n81\t return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n82\t return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n83\t}\n84\t\n85\t/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\n86\tfunction badgesConfirmes(l: Listing): string[] {\n87\t const d = l.details ?? {};\n88\t const out: string[] = [];\n89\t const inc = d.inclusions ?? {};\n90\t if (inc.heating) out.push(\"Chauffage inclus\");\n91\t if (inc.electricity) out.push(\"Électricité incluse\");\n92\t if (inc.hot_water) out.push(\"Eau chaude incluse\");\n93\t if (inc.internet) out.push(\"Internet inclus\");\n94\t const app = d.appliances ?? {};\n95\t if (app.dishwasher) out.push(\"Lave-vaisselle\");\n96\t if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n97\t if (app.fridge && app.stove) out.push(\"Électroménagers\");\n98\t if (d.ac) out.push(\"Climatisation\");\n99\t if (d.elevator) out.push(\"Ascenseur\");\n100\t if (d.balcony) out.push(\"Balcon\");\n101\t if (d.pool) out.push(\"Piscine\");\n102\t if (d.gym) out.push(\"Gym\");\n103\t if (d.laundry) out.push(\"Buanderie\");\n104\t if (d.storage) out.push(\"Rangement\");\n105\t if (d.parking?.available)\n106\t out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n107\t if (l.furnished) out.push(\"Meublé\");\n108\t if (d.smoking === false) out.push(\"Non-fumeur\");\n109\t return out;\n110\t}\n111\t\n112\t// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\n113\tfunction Lightbox({ images, start, titre, onClose }:\n114\t { images: string[]; start: number; titre: string; onClose: () => void }) {\n115\t const [idx, setIdx] = useState(start);\n116\t const [scale, setScale] = useState(1);\n117\t const [tx, setTx] = useState(0);\n118\t const [ty, setTy] = useState(0);\n119\t const track = useRef(null);\n120\t const pointers = useRef(new Map());\n121\t const pinch = useRef<{ d: number; scale: number } | null>(null);\n122\t const lastTap = useRef(0);\n123\t\n124\t useEffect(() => { // position initiale + verrou du défilement de la page\n125\t track.current?.scrollTo({ left: start * track.current.clientWidth });\n126\t document.body.style.overflow = \"hidden\";\n127\t const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n128\t window.addEventListener(\"keydown\", onKey);\n129\t return () => {\n130\t document.body.style.overflow = \"\";\n131\t window.removeEventListener(\"keydown\", onKey);\n132\t };\n133\t // eslint-disable-next-line react-hooks/exhaustive-deps\n134\t }, []);\n135\t\n136\t const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n137\t const onScroll = () => {\n138\t const el = track.current;\n139\t if (el && scale === 1) {\n140\t const i = Math.round(el.scrollLeft / el.clientWidth);\n141\t if (i !== idx) { setIdx(i); resetZoom(); }\n142\t }\n143\t };\n144\t const dist = () => {\n145\t const [a, b] = [...pointers.current.values()];\n146\t return Math.hypot(a.x - b.x, a.y - b.y);\n147\t };\n148\t const onPointerDown = (e: React.PointerEvent) => {\n149\t pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n150\t if (pointers.current.size === 2)\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b4c816ad-ef0a-4cb9-9621-46d5f3142fd8","timestamp":"2026-08-24T21:45:23.230Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running ls -la /Users/simon-pierreboucher/apps/lou-ka/fro…","usage":{"total_tokens":68626,"tool_uses":37,"duration_ms":38020},"last_tool_name":"Bash","uuid":"a248c2c6-c8e5-4091-944a-77b38a42ad7d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgknoegJiFLn3W3RhT","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013CYpAfkneD4Z9zbmxTT4oL","name":"Bash","input":{"command":"ls -la /Users/simon-pierreboucher/apps/lou-ka/frontend/src/components"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1457,"cache_read_input_tokens":67039,"cache_creation":{"ephemeral_5m_input_tokens":1457,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0f936c31-30ae-4423-a507-cd101946b8d1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013CYpAfkneD4Z9zbmxTT4oL","type":"tool_result","content":"total 280\ndrwxr-xr-x 26 simon-pierreboucher staff 832 Aug 24 16:23 .\ndrwxr-xr-x 15 simon-pierreboucher staff 480 Aug 24 16:47 ..\n-rw-r--r-- 1 simon-pierreboucher staff 3388 Aug 23 22:35 CommercesProches.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 4949 Aug 18 16:44 CookieConsent.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 1729 Aug 22 19:45 CtFicheMap.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 2251 Aug 22 19:43 CtListingCard.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 4263 Aug 22 19:44 CtMapView.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3208 Aug 23 22:16 EssenceProche.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 1698 Aug 18 17:18 FairValueBadge.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 4668 Aug 24 16:23 HistoriqueTAL.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3263 Aug 23 23:28 HydroEstimation.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 6361 Aug 15 15:21 Icons.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2598 Aug 18 18:34 KaScoreBadge.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 5240 Aug 18 18:36 KaScoresBlock.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2578 Aug 18 18:35 ListingCard.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 2302 Aug 23 22:29 ListingMap3D.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 4894 Aug 15 15:48 Logo.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 5955 Aug 23 22:29 MapView.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2233 Aug 23 22:29 MetroLignes.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 2610 Aug 13 00:08 Pager.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 5317 Aug 18 17:18 PriceAnalysis.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3645 Aug 23 22:16 QualiteAir.tsx\n-rw-r--r--@ 1 simon-pierreboucher staff 6908 Aug 23 22:08 QuartierBlock.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 9957 Aug 23 22:04 RegistreLoyers.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 3415 Aug 23 22:00 RisqueInondation.tsx\n-rw-r--r-- 1 simon-pierreboucher staff 1522 Aug 18 15:39 SmartImg.tsx","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"69ee46ef-0e66-47ff-954e-be42e97f0dea","timestamp":"2026-08-24T21:45:23.335Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":72857,"tool_uses":38,"duration_ms":40032},"last_tool_name":"Bash","uuid":"dcdd630e-c956-4cd4-9d82-f8c4baafbf00","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgtzfiNPU9xyTrVpm3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XCDPdYjuASfqqPAKoeu5HN","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;\" 2>&1 | head -50"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4225,"cache_read_input_tokens":68496,"cache_creation":{"ephemeral_5m_input_tokens":4225,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d12ef8de-375b-4bd3-9a64-393c0e029b1a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XCDPdYjuASfqqPAKoeu5HN","type":"tool_result","content":"detail_cache\nenv_tiles\nfairvalue\nfavorites\nfv_segments\ngeocode_cache\nimage_checks\nkascores\nlistings\npoi_cache\nprice_log\nsource_profiles\nsqlite_sequence\nsqlite_stat1\nsqlite_stat4\nsync_log\nusers","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6ae49247-784b-4390-ac50-39d857100de9","timestamp":"2026-08-24T21:45:25.365Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":72860,"tool_uses":39,"duration_ms":40630},"last_tool_name":"Bash","uuid":"e8a4f850-afd1-4689-bfaf-bb8a442bd0b0","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgtzfiNPU9xyTrVpm3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BQwhDpR9wSorRsoQRPMsnt","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(*) as nb_listings FROM listings WHERE active=1;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4225,"cache_read_input_tokens":68496,"cache_creation":{"ephemeral_5m_input_tokens":4225,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8de1ec0b-4edc-47f6-9fe1-2f5c49cb8ee0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BQwhDpR9wSorRsoQRPMsnt","type":"tool_result","content":"60999","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5a18d74e-7e67-4062-a7e5-109e7ad8c3c8","timestamp":"2026-08-24T21:45:25.957Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":72863,"tool_uses":40,"duration_ms":40683},"last_tool_name":"Bash","uuid":"56ad3e84-be54-49a0-8207-8823cffccc46","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJgtzfiNPU9xyTrVpm3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NRz4G1iCBWr2AH4ELBFUTh","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(*) as nb_published FROM listings WHERE active=1 AND published=1;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":4225,"cache_read_input_tokens":68496,"cache_creation":{"ephemeral_5m_input_tokens":4225,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a1c74a79-9de3-4b14-9cbd-b7825003bfc4"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NRz4G1iCBWr2AH4ELBFUTh","type":"tool_result","content":"59819","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1578489a-5275-4eab-8925-08a9e96d94f5","timestamp":"2026-08-24T21:45:25.997Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":73309,"tool_uses":41,"duration_ms":41995},"last_tool_name":"Bash","uuid":"be8e2b77-019f-4fe4-b8ba-275c78dd2f93","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJh6PH4RLWKQFg6Rwjk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019STjzo9NnGVZDWiVDsSXAQ","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"PRAGMA table_info(listings);\" 2>&1 | head -60"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":442,"cache_read_input_tokens":72721,"cache_creation":{"ephemeral_5m_input_tokens":442,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"af038998-9c9b-46f5-9853-2abc4bf3516a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019STjzo9NnGVZDWiVDsSXAQ","type":"tool_result","content":"0|uid|TEXT|0||1\n1|source|TEXT|1||0\n2|external_id|TEXT|1||0\n3|url|TEXT|0||0\n4|title|TEXT|0||0\n5|address|TEXT|0||0\n6|sector|TEXT|0||0\n7|city|TEXT|0||0\n8|unit_type|TEXT|0||0\n9|price|REAL|0||0\n10|price_label|TEXT|0||0\n11|availability|TEXT|0||0\n12|description|TEXT|0||0\n13|amenities|TEXT|0||0\n14|images|TEXT|0||0\n15|lat|REAL|0||0\n16|lng|REAL|0||0\n17|content_hash|TEXT|0||0\n18|first_seen|REAL|0||0\n19|last_seen|REAL|0||0\n20|updated_at|REAL|0||0\n21|active|INTEGER|0|1|0\n22|availability_date|TEXT|0||0\n23|area_sqft|REAL|0||0\n24|pets|TEXT|0||0\n25|furnished|INTEGER|0||0\n26|details|TEXT|0||0\n27|geocode_failed|INTEGER|0|0|0\n28|miss_count|INTEGER|0|0|0\n29|dauid|TEXT|0||0\n30|digest|TEXT|0||0\n31|dup_of|TEXT|0||0\n32|dup_sources|TEXT|0||0\n33|bedrooms|REAL|0||0\n34|bathrooms|REAL|0||0\n35|completeness|REAL|0||0\n36|published|INTEGER|0|1|0\n37|quality_reasons|TEXT|0||0\n38|images_ok|TEXT|0||0\n39|img_audit|TEXT|0||0\n40|coord_key|TEXT|0||0","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"65e4b5ec-e413-435f-8928-ae565140b1df","timestamp":"2026-08-24T21:45:27.323Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":73311,"tool_uses":42,"duration_ms":42312},"last_tool_name":"Bash","uuid":"432e5545-2919-4e25-9a65-0dd1c797d6bc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJh6PH4RLWKQFg6Rwjk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016ecEV2cbqwc9f9s98wcobp","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(*) FROM price_log;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":442,"cache_read_input_tokens":72721,"cache_creation":{"ephemeral_5m_input_tokens":442,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"11628a19-b5e6-434d-b4bd-4df7d5c656e0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016ecEV2cbqwc9f9s98wcobp","type":"tool_result","content":"95767","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fba9d858-0957-45d6-b3f5-a9282d489159","timestamp":"2026-08-24T21:45:27.643Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":73313,"tool_uses":43,"duration_ms":42527},"last_tool_name":"Bash","uuid":"eba9b8e2-8996-4465-a89b-4730672e1e59","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJh6PH4RLWKQFg6Rwjk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KJr7xgGtfnmnhLHVvDJEBk","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(DISTINCT uid) FROM price_log;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":442,"cache_read_input_tokens":72721,"cache_creation":{"ephemeral_5m_input_tokens":442,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a14b4e67-c5a4-4345-8dde-b99282601d96"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KJr7xgGtfnmnhLHVvDJEBk","type":"tool_result","content":"91302","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8fda0394-8235-4308-a217-2378867cd3f5","timestamp":"2026-08-24T21:45:27.861Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":74243,"tool_uses":44,"duration_ms":44422},"last_tool_name":"Bash","uuid":"ee012a8b-2564-4936-9c74-6a0ebbda584c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJhF2vreB5xogD7mubh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XCdsizWBTbxySfpfahZgXj","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"PRAGMA table_info(fairvalue);\" && sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(*) FROM fairvalue;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":926,"cache_read_input_tokens":73163,"cache_creation":{"ephemeral_5m_input_tokens":926,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0822a983-55c8-4461-9c95-9a3c7fd0e06a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XCdsizWBTbxySfpfahZgXj","type":"tool_result","content":"0|uid|TEXT|0||1\n1|fv|REAL|0||0\n2|fv_low|REAL|0||0\n3|fv_high|REAL|0||0\n4|deviation|REAL|0||0\n5|verdict|TEXT|0||0\n6|confidence|TEXT|0||0\n7|method|TEXT|0||0\n8|comps|INTEGER|0||0\n9|segment|TEXT|0||0\n10|model_version|TEXT|0||0\n11|computed_at|REAL|0||0\n45709","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"031b2ab1-f713-4349-9eb2-005c0d3c9c86","timestamp":"2026-08-24T21:45:29.760Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":74245,"tool_uses":45,"duration_ms":45305},"last_tool_name":"Bash","uuid":"f5c42bb2-2adf-4ecd-8ac5-5d7d4eac5e30","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJhF2vreB5xogD7mubh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QVDpsb1ymjVh5D4i8X8biY","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"PRAGMA table_info(kascores);\" && sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(*) FROM kascores;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":926,"cache_read_input_tokens":73163,"cache_creation":{"ephemeral_5m_input_tokens":926,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"42ea5d31-567f-4c5d-9271-e187bc208c9c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QVDpsb1ymjVh5D4i8X8biY","type":"tool_result","content":"0|coord_key|TEXT|0||1\n1|lat|REAL|0||0\n2|lng|REAL|0||0\n3|walk|REAL|0||0\n4|transit|REAL|0||0\n5|bike|REAL|0||0\n6|calme|REAL|0||0\n7|services|REAL|0||0\n8|global|REAL|0||0\n9|details|TEXT|0||0\n10|version|TEXT|0||0\n11|computed_at|REAL|0||0\n28438","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"128e5bc6-5fa6-445a-88f6-4d24d4f04cf0","timestamp":"2026-08-24T21:45:30.619Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Running sqlite3 /Users/simon-pierreboucher/apps/lou-ka/da…","usage":{"total_tokens":74247,"tool_uses":46,"duration_ms":45344},"last_tool_name":"Bash","uuid":"c6dcd1a5-97f5-470d-ad44-b024c7fe4a52","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJhF2vreB5xogD7mubh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ViC2NMkQ7XRHBrEgjrrkX1","name":"Bash","input":{"command":"sqlite3 /Users/simon-pierreboucher/apps/lou-ka/data/louka.db \"SELECT COUNT(*) FROM sync_log;\" 2>&1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":926,"cache_read_input_tokens":73163,"cache_creation":{"ephemeral_5m_input_tokens":926,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"034d6999-89f7-46b7-8d5e-0fe8e51249fa"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ViC2NMkQ7XRHBrEgjrrkX1","type":"tool_result","content":"39058","is_error":false}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"489fd878-d698-4d95-b97e-4876ca71743e","timestamp":"2026-08-24T21:45:30.655Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/web.py","usage":{"total_tokens":74944,"tool_uses":47,"duration_ms":47047},"last_tool_name":"Read","uuid":"fa7857f3-c079-4773-88b0-eca32c041967","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJhSJqUiKRt3VsxKKUU","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PRXYCyM63UWUHUF9qfan8e","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","offset":150,"limit":250},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":693,"cache_read_input_tokens":74089,"cache_creation":{"ephemeral_5m_input_tokens":693,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7819cd49-253c-4c38-b316-ddb6ce677d20"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PRXYCyM63UWUHUF9qfan8e","type":"tool_result","content":"150\t \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n151\t elif sort == \"prix\":\n152\t sql += \" ORDER BY price IS NULL, price ASC LIMIT ? OFFSET ?\"\n153\t else: # défaut : les plus récentes d'abord\n154\t sql += \" ORDER BY first_seen DESC, price IS NULL, price ASC LIMIT ? OFFSET ?\"\n155\t args += [limit, offset]\n156\t rows = [_row_to_dict(r) for r in con.execute(sql, args).fetchall()]\n157\t con.close()\n158\t return {\"total\": total, \"count\": len(rows), \"listings\": rows}\n159\t\n160\t\n161\t@app.get(\"/api/listings.geojson\")\n162\tdef listings_geojson(\n163\t city: str | None = None,\n164\t sector: str | None = None,\n165\t unit_type: str | None = None,\n166\t source: str | None = None,\n167\t price_max: float | None = None,\n168\t price_min: float | None = None,\n169\t pets: str | None = None,\n170\t furnished: int | None = None,\n171\t available_by: str | None = None,\n172\t area_min: float | None = None,\n173\t q: str | None = None,\n174\t bbox: str | None = None,\n175\t limit: int = Query(3000, le=8000),\n176\t):\n177\t \"\"\"Annonces géolocalisées au format GeoJSON (mêmes filtres que /api/listings).\n178\t\n179\t Léger par conception : seuls les champs nécessaires aux marqueurs et aux\n180\t fiches de la carte sont inclus (1 photo, pas de description).\n181\t\n182\t Ka Maps : `bbox=ouest,sud,est,nord` restreint au rectangle visible\n183\t (index idx_listings_geo) ; `limit` plafonne la réponse. La collection\n184\t porte deux membres additionnels : `totalGeocoded` (annonces géolocalisées\n185\t correspondant aux filtres) et `totalMatching` (avec ou sans coordonnées),\n186\t pour afficher « N résultats sans position sur la carte ».\n187\t \"\"\"\n188\t con = db.connect()\n189\t sql = (\"SELECT uid, title, address, price, price_label, unit_type,\"\n190\t \" availability_date, source, city, sector, area_sqft, images,\"\n191\t \" images_ok, lat, lng, fv.verdict AS fv_verdict,\"\n192\t \" fv.deviation AS fv_deviation\"\n193\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n194\t \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n195\t \" AND lat IS NOT NULL AND lng IS NOT NULL\")\n196\t args: list = []\n197\t if bbox:\n198\t try:\n199\t west, south, east, north = (float(v) for v in bbox.split(\",\"))\n200\t except ValueError:\n201\t raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n202\t sql += \" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\"\n203\t args += [south, north, west, east]\n204\t if city:\n205\t sql += \" AND city=?\"; args.append(city)\n206\t if sector:\n207\t sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n208\t if unit_type:\n209\t sql += \" AND unit_type=?\"; args.append(unit_type)\n210\t if source:\n211\t sql += \" AND source=?\"; args.append(source)\n212\t if price_max is not None:\n213\t sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n214\t if price_min is not None:\n215\t sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n216\t if pets == \"oui\":\n217\t sql += \" AND pets IN ('oui','conditions')\"\n218\t elif pets:\n219\t sql += \" AND pets=?\"; args.append(pets)\n220\t if furnished in (0, 1):\n221\t sql += \" AND furnished=?\"; args.append(furnished)\n222\t if available_by:\n223\t sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n224\t \" AND availability_date<=?))\"\n225\t args.append(available_by)\n226\t if area_min is not None:\n227\t sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n228\t if q:\n229\t sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n230\t args += [f\"%{q}%\"] * 3\n231\t\n232\t total_geo = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n233\t # Même requête sans l'exigence de coordonnées ni le bbox : mesure les\n234\t # annonces filtrées invisibles sur la carte (affordance « sans position »).\n235\t sql_all = sql.replace(\" AND lat IS NOT NULL AND lng IS NOT NULL\", \"\")\n236\t args_all = list(args)\n237\t if bbox:\n238\t sql_all = sql_all.replace(\" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\", \"\")\n239\t del args_all[0:4]\n240\t total_all = con.execute(f\"SELECT COUNT(*) c FROM ({sql_all})\", args_all).fetchone()[\"c\"]\n241\t\n242\t sql += \" ORDER BY first_seen DESC LIMIT ?\"\n243\t args.append(limit)\n244\t\n245\t features = []\n246\t for r in con.execute(sql, args).fetchall():\n247\t # galerie nettoyée si auditée (même vide : les images brutes sont\n248\t # alors toutes mortes/placeholder — le frontend affiche le visuel\n249\t # de secours Lou-Ka plutôt qu'une image cassée)\n250\t images = json.loads((r[\"images_ok\"] if r[\"images_ok\"] is not None\n251\t else r[\"images\"]) or \"[]\")\n252\t features.append({\n253\t \"type\": \"Feature\",\n254\t \"geometry\": {\"type\": \"Point\", \"coordinates\": [r[\"lng\"], r[\"lat\"]]},\n255\t \"properties\": {\n256\t \"uid\": r[\"uid\"], \"title\": r[\"title\"], \"address\": r[\"address\"],\n257\t \"price\": r[\"price\"],\n258\t \"price_label\": r[\"price_label\"], \"unit_type\": r[\"unit_type\"],\n259\t \"availability_date\": r[\"availability_date\"],\n260\t \"source\": r[\"source\"], \"city\": r[\"city\"], \"sector\": r[\"sector\"],\n261\t \"area_sqft\": r[\"area_sqft\"],\n262\t \"fv_verdict\": r[\"fv_verdict\"], \"fv_deviation\": r[\"fv_deviation\"],\n263\t \"image\": images[0] if images else None,\n264\t },\n265\t })\n266\t con.close()\n267\t return {\"type\": \"FeatureCollection\", \"features\": features,\n268\t \"totalGeocoded\": total_geo, \"totalMatching\": total_all}\n269\t\n270\t\n271\t# --- Recherche unifiée liste + carte -----------------------------------------\n272\t# Une seule vérité partagée : la même requête sert la page de liste ET les\n273\t# points de la carte, dans la même réponse. Le compteur affiché en tête de\n274\t# liste est par construction égal au nombre de points envoyés à la carte.\n275\t\n276\t_SEARCH_SORTS = {\n277\t \"prix\": \" ORDER BY price IS NULL, price ASC\",\n278\t \"prix_desc\": \" ORDER BY price IS NULL, price DESC\",\n279\t \"recent\": \" ORDER BY first_seen DESC, price IS NULL, price ASC\",\n280\t \"deal\": (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n281\t \" price IS NULL, price ASC\"),\n282\t \"ka\": (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n283\t \" price IS NULL, price ASC\"),\n284\t}\n285\t\n286\t# colonnes KA Scores jointes aux annonces (pastilles + tri + fiche)\n287\t_KS_COLS = (\" ks.walk AS ks_walk, ks.transit AS ks_transit,\"\n288\t \" ks.bike AS ks_bike, ks.calme AS ks_calme,\"\n289\t \" ks.services AS ks_services, ks.global AS ks_global\")\n290\t_KS_JOIN = \" LEFT JOIN kascores ks ON ks.coord_key = listings.coord_key\"\n291\t_FV_CODES = {\"sous\": \"s\", \"marche\": \"m\", \"sur\": \"o\"}\n292\t\n293\t\n294\tdef _parse_poly(poly: str) -> list[tuple[float, float]]:\n295\t \"\"\"`poly=lng,lat;lng,lat;…` → liste de sommets (≥ 3) ou HTTP 400.\"\"\"\n296\t try:\n297\t pts = [tuple(float(v) for v in p.split(\",\")) for p in poly.split(\";\") if p]\n298\t except ValueError:\n299\t raise HTTPException(400, \"poly attendu : lng,lat;lng,lat;…\")\n300\t if len(pts) < 3 or any(len(p) != 2 for p in pts):\n301\t raise HTTPException(400, \"poly : au moins 3 sommets lng,lat\")\n302\t return pts # type: ignore[return-value]\n303\t\n304\t\n305\tdef _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n306\t \"\"\"Ray casting — même algorithme que ka-maps (utils/geo.pointInPolygon).\"\"\"\n307\t inside = False\n308\t j = len(ring) - 1\n309\t for i in range(len(ring)):\n310\t xi, yi = ring[i]\n311\t xj, yj = ring[j]\n312\t if (yi > lat) != (yj > lat) and lng < (xj - xi) * (lat - yi) / (yj - yi) + xi:\n313\t inside = not inside\n314\t j = i\n315\t return inside\n316\t\n317\t\n318\t@app.get(\"/api/search\")\n319\tdef search_unified(\n320\t city: str | None = None,\n321\t sector: str | None = None,\n322\t unit_type: str | None = None,\n323\t source: str | None = None,\n324\t price_max: float | None = None,\n325\t price_min: float | None = None,\n326\t pets: str | None = None,\n327\t furnished: int | None = None,\n328\t available_by: str | None = None,\n329\t area_min: float | None = None,\n330\t q: str | None = None,\n331\t deal: str | None = None, # sous | marche | sur (juste valeur)\n332\t bbox: str | None = None, # ouest,sud,est,nord (zone visible carte)\n333\t poly: str | None = None, # lng,lat;… (zone dessinée)\n334\t kascore_min: float | None = None, # KA Score global minimal (0-100)\n335\t sort: str = \"recent\", # recent (défaut) | prix | prix_desc | deal | ka\n336\t page: int = 1,\n337\t page_size: int = Query(20, ge=1, le=100),\n338\t include: str = \"tout\", # tout | liste (points inchangés côté client)\n339\t):\n340\t \"\"\"Recherche synchronisée liste ↔ carte (une réponse = les deux vues).\n341\t\n342\t Retourne `total` (compteur partagé), `listings` (la page demandée) et\n343\t `points` (TOUS les points correspondants, format compact\n344\t [uid, lng, lat, prix, verdict]) — triés selon `sort`, si bien que la\n345\t position d'un point dans `points` donne directement sa page de liste.\n346\t Seules les annonces géolocalisées participent (la règle d'or : chaque\n347\t annonce de la liste est visible sur la carte) ; `unpositioned` compte\n348\t les annonces filtrées sans coordonnées, pour l'afficher honnêtement.\n349\t \"\"\"\n350\t if sort not in _SEARCH_SORTS:\n351\t raise HTTPException(400, f\"sort inconnu : {sort}\")\n352\t ring = _parse_poly(poly) if poly else None\n353\t\n354\t con = db.connect()\n355\t sql = (\" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n356\t \" WHERE dup_of IS NULL AND active=1 AND published=1\"\n357\t \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\")\n358\t args: list = []\n359\t if bbox:\n360\t try:\n361\t west, south, east, north = (float(v) for v in bbox.split(\",\"))\n362\t except ValueError:\n363\t raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n364\t sql += (\" AND listings.lat BETWEEN ? AND ?\"\n365\t \" AND listings.lng BETWEEN ? AND ?\")\n366\t args += [south, north, west, east]\n367\t if ring:\n368\t # préfiltre SQL par l'emprise du polygone, appartenance exacte en aval\n369\t lngs = [p[0] for p in ring]; lats = [p[1] for p in ring]\n370\t sql += (\" AND listings.lat BETWEEN ? AND ?\"\n371\t \" AND listings.lng BETWEEN ? AND ?\")\n372\t args += [min(lats), max(lats), min(lngs), max(lngs)]\n373\t if city:\n374\t sql += \" AND city=?\"; args.append(city)\n375\t if sector:\n376\t sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n377\t if unit_type:\n378\t sql += \" AND unit_type=?\"; args.append(unit_type)\n379\t if source:\n380\t sql += \" AND source=?\"; args.append(source)\n381\t if price_max is not None:\n382\t sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n383\t if price_min is not None:\n384\t sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n385\t if pets == \"oui\":\n386\t sql += \" AND pets IN ('oui','conditions')\"\n387\t elif pets:\n388\t sql += \" AND pets=?\"; args.append(pets)\n389\t if furnished in (0, 1):\n390\t sql += \" AND furnished=?\"; args.append(furnished)\n391\t if available_by:\n392\t sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n393\t \" AND availability_date<=?))\"\n394\t args.append(available_by)\n395\t if area_min is not None:\n396\t sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n397\t if q:\n398\t sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n399\t args += [f\"%{q}%\"] * 3\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"992657e8-fcf1-4364-8973-8c3ea390cc05","timestamp":"2026-08-24T21:45:32.349Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading louka/web.py","usage":{"total_tokens":79273,"tool_uses":48,"duration_ms":49023},"last_tool_name":"Read","uuid":"3288638b-efea-4311-8ee2-96066f153606","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJhZg6jXHbnDDjhVjx3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014FeYqCjajSH6QP62UN6nXE","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","offset":450,"limit":350},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":4326,"cache_read_input_tokens":74782,"cache_creation":{"ephemeral_5m_input_tokens":4326,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7792e222-55ab-4121-ad38-ca21fcd94f76"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014FeYqCjajSH6QP62UN6nXE","type":"tool_result","content":"450\t return {\n451\t \"total\": total, \"page\": page, \"page_size\": page_size, \"sort\": sort,\n452\t \"listings\": listings,\n453\t \"points\": points if include != \"liste\" else None,\n454\t \"unpositioned\": unpositioned,\n455\t }\n456\t\n457\t\n458\t# --- Miniatures optimisées (WebP, taille écran) ------------------------------\n459\t# Sert les images des annonces redimensionnées et compressées (perf mobile) :\n460\t# /api/img?u=&w=480. Seules les URLs déjà vérifiées par le contrôle\n461\t# qualité (table image_checks) sont proxifiées — pas de proxy ouvert. Si\n462\t# l'URL n'est pas (encore) connue : 404, le frontend retombe sur l'original.\n463\t_IMG_WIDTHS = (160, 480, 800, 1280)\n464\t_IMG_CACHE = ROOT / \"data\" / \"imgcache\"\n465\t\n466\t\n467\t@app.get(\"/api/img\")\n468\tdef img_thumbnail(u: str, w: int = 480):\n469\t import hashlib\n470\t import requests as _rq\n471\t from fastapi.responses import Response\n472\t w = min(_IMG_WIDTHS, key=lambda x: abs(x - w))\n473\t if not u.startswith((\"http://\", \"https://\")):\n474\t raise HTTPException(400, \"URL invalide\")\n475\t key = hashlib.sha1(u.encode()).hexdigest()\n476\t _IMG_CACHE.mkdir(parents=True, exist_ok=True)\n477\t path = _IMG_CACHE / f\"{key}_{w}.webp\"\n478\t headers = {\"Cache-Control\": \"public, max-age=2592000, immutable\"}\n479\t if path.exists():\n480\t return Response(path.read_bytes(), media_type=\"image/webp\",\n481\t headers=headers)\n482\t con = db.connect()\n483\t known = con.execute(\"SELECT status FROM image_checks WHERE url=?\",\n484\t (u,)).fetchone()\n485\t con.close()\n486\t if known is None or known[\"status\"] not in (\"ok\",):\n487\t raise HTTPException(404, \"image inconnue du contrôle qualité\")\n488\t try:\n489\t resp = _rq.get(u, timeout=12, headers={\"User-Agent\": \"LouKaImg/1.0\"})\n490\t resp.raise_for_status()\n491\t from io import BytesIO\n492\t from PIL import Image, ImageOps\n493\t img = Image.open(BytesIO(resp.content))\n494\t img = ImageOps.exif_transpose(img)\n495\t if img.mode not in (\"RGB\", \"L\"):\n496\t img = img.convert(\"RGB\")\n497\t if img.width > w:\n498\t img = img.resize((w, max(1, round(img.height * w / img.width))),\n499\t Image.LANCZOS)\n500\t buf = BytesIO()\n501\t img.save(buf, \"WEBP\", quality=80, method=4)\n502\t data = buf.getvalue()\n503\t path.write_bytes(data)\n504\t return Response(data, media_type=\"image/webp\", headers=headers)\n505\t except HTTPException:\n506\t raise\n507\t except Exception:\n508\t raise HTTPException(404, \"image irrécupérable\")\n509\t\n510\t\n511\t@app.get(\"/api/listings/{uid}/pdf\")\n512\tdef listing_pdf(uid: str):\n513\t \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n514\t from fastapi.responses import Response\n515\t from . import pdfgen\n516\t data = pdfgen.fiche_pdf(uid)\n517\t if data is None:\n518\t raise HTTPException(404, \"Annonce introuvable\")\n519\t nom = uid.replace(\":\", \"-\")\n520\t return Response(content=data, media_type=\"application/pdf\", headers={\n521\t \"Content-Disposition\": f'attachment; filename=\"louka-fiche-{nom}.pdf\"'})\n522\t\n523\t\n524\t@app.get(\"/api/stats/rapport.pdf\")\n525\tdef rapport_marche_pdf():\n526\t \"\"\"Rapport global du marché locatif (PDF multi-pages).\"\"\"\n527\t from fastapi.responses import Response\n528\t from . import pdfgen\n529\t return Response(content=pdfgen.rapport_pdf(), media_type=\"application/pdf\",\n530\t headers={\"Content-Disposition\":\n531\t 'attachment; filename=\"louka-rapport-marche.pdf\"'})\n532\t\n533\t\n534\t@app.get(\"/api/fairvalue/{uid}\")\n535\tdef fairvalue_detail(uid: str):\n536\t \"\"\"Analyse de prix Lou-Ka : juste valeur, fourchette, confiance,\n537\t distribution du segment (voir louka/fairvalue.py).\"\"\"\n538\t from . import fairvalue\n539\t d = fairvalue.explain(uid)\n540\t if d is None:\n541\t raise HTTPException(404, \"Pas d'estimation pour cette annonce\")\n542\t return d\n543\t\n544\t\n545\t@app.get(\"/api/hydro\")\n546\tdef hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n547\t lat: float | None = None, lng: float | None = None):\n548\t \"\"\"Estimation du coût d'électricité à une adresse (Hydro-Québec).\n549\t Par défaut cache seulement (aucun captcha) ; solve=1 force le calcul.\n550\t uid/lat/lng permettent la lecture cache par annonce et la dérivation du\n551\t code postal (adresses sans CP).\"\"\"\n552\t from . import hydro\n553\t return hydro.estimate(adresse=adresse, uid=uid, lat=lat, lng=lng,\n554\t solve=bool(solve))\n555\t\n556\t\n557\t@app.get(\"/api/commerces\")\n558\tdef commerces_at(lat: float, lng: float):\n559\t \"\"\"Grands commerces + métro/bus les plus proches (Mapbox / OSM).\"\"\"\n560\t from . import commerces\n561\t return commerces.nearby(lat, lng)\n562\t\n563\t\n564\t@app.get(\"/api/air\")\n565\tdef air_at(lat: float, lng: float):\n566\t \"\"\"Qualité de l'air : station RSQAQ la plus proche (MELCCFP).\"\"\"\n567\t from . import air\n568\t d = air.lookup(lat, lng)\n569\t if d is None:\n570\t raise HTTPException(404, \"Base qualité de l'air non disponible\")\n571\t return d\n572\t\n573\t\n574\t@app.get(\"/api/gaz\")\n575\tdef gaz_at(lat: float, lng: float):\n576\t \"\"\"Stations-service à proximité et prix courants (gazquebec.ca).\"\"\"\n577\t from . import gaz\n578\t return gaz.nearby(lat, lng)\n579\t\n580\t\n581\t@app.get(\"/api/inondation\")\n582\tdef inondation_at(lat: float, lng: float):\n583\t \"\"\"Risque d'inondation à l'adresse (BDZI, gouv. du Québec) —\n584\t bloc « Risque d'inondation » de la fiche.\"\"\"\n585\t from . import inondation\n586\t d = inondation.lookup(lat, lng)\n587\t if d is None:\n588\t raise HTTPException(404, \"Base des zones inondables non disponible\")\n589\t return d\n590\t\n591\t\n592\t@app.get(\"/api/rdl\")\n593\tdef rdl_nearby(lat: float, lng: float, radius: int = 600):\n594\t \"\"\"Loyers déclarés au Registre des loyers (registre-des-loyers.ca)\n595\t autour d'un point — bloc « Registre des loyers » de la fiche.\"\"\"\n596\t from . import rdl\n597\t radius = max(100, min(radius, 2000))\n598\t d = rdl.nearby(lat, lng, radius)\n599\t if d is None:\n600\t raise HTTPException(404, \"Registre des loyers non disponible\")\n601\t return d\n602\t\n603\t\n604\t@app.get(\"/api/tal\")\n605\tdef tal_history(address: str, city: str | None = None):\n606\t \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n607\t à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n608\t Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n609\t from . import tal\n610\t return tal.lookup(address, city)\n611\t\n612\t\n613\t@app.get(\"/api/listings/{uid}\")\n614\tdef get_listing(uid: str):\n615\t con = db.connect()\n616\t row = con.execute(\n617\t \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n618\t \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n619\t \" fv.confidence AS fv_confidence\"\n620\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n621\t \" WHERE uid=?\", (uid,)).fetchone()\n622\t d = None\n623\t if row is not None:\n624\t d = _row_to_dict(row)\n625\t # commodités de proximité (cache par immeuble, voir louka/poi.py)\n626\t if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n627\t key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n628\t poi_row = con.execute(\n629\t \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n630\t d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n631\t else:\n632\t d[\"poi\"] = []\n633\t # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n634\t if d.get(\"coord_key\"):\n635\t ks = con.execute(\n636\t \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n637\t \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n638\t (d[\"coord_key\"],)).fetchone()\n639\t if ks is not None:\n640\t d[\"kascores\"] = {\n641\t \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n642\t \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n643\t \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n644\t \"details\": json.loads(ks[\"details\"] or \"{}\"),\n645\t \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n646\t }\n647\t # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n648\t from . import quartier\n649\t dauid = d.get(\"dauid\")\n650\t d[\"quartier\"] = quartier.fiche_quartier(\n651\t d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n652\t dauid if dauid and dauid != \"hors-zone\" else None)\n653\t # description structurée + historique de prix\n654\t d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n655\t d[\"price_history\"] = [dict(r) for r in con.execute(\n656\t \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n657\t (uid,)).fetchall()]\n658\t con.close()\n659\t if d is None:\n660\t raise HTTPException(404, \"Annonce introuvable\")\n661\t return d\n662\t\n663\t\n664\t@app.get(\"/api/kascores/stats\")\n665\tdef kascores_stats():\n666\t \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n667\t from . import kascores\n668\t return kascores.stats()\n669\t\n670\t\n671\t@app.get(\"/api/facets\")\n672\tdef facets(city: str | None = None):\n673\t \"\"\"Valeurs distinctes pour construire les filtres du frontend.\n674\t\n675\t `city` (optionnel) restreint la liste des quartiers à cette ville —\n676\t utilisé par le sélecteur « Quartier » dépendant de « Ville ».\n677\t \"\"\"\n678\t con = db.connect()\n679\t sector_sql = (\"SELECT DISTINCT sector FROM listings\"\n680\t \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n681\t \" AND sector<>''\")\n682\t sector_args: list = []\n683\t if city:\n684\t sector_sql += \" AND city=?\"\n685\t sector_args.append(city)\n686\t out = {\n687\t \"cities\": [r[\"city\"] for r in con.execute(\n688\t \"SELECT DISTINCT city FROM listings WHERE active=1 AND published=1\"\n689\t \" AND dup_of IS NULL AND city<>'' ORDER BY city\")],\n690\t \"sectors\": [r[\"sector\"] for r in con.execute(\n691\t sector_sql + \" ORDER BY sector\", sector_args)],\n692\t \"unit_types\": [r[\"unit_type\"] for r in con.execute(\n693\t \"SELECT DISTINCT unit_type FROM listings WHERE active=1 AND published=1\"\n694\t \" AND dup_of IS NULL AND unit_type<>'' ORDER BY unit_type\")],\n695\t \"sources\": [dict(r) for r in con.execute(\n696\t \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n697\t \" AND dup_of IS NULL GROUP BY source ORDER BY n DESC\")],\n698\t }\n699\t con.close()\n700\t return out\n701\t\n702\t\n703\t@app.get(\"/api/sources\")\n704\tdef sources():\n705\t registry = json.loads(SOURCES_PATH.read_text(encoding=\"utf-8\"))[\"sources\"]\n706\t con = db.connect()\n707\t counts = {r[\"source\"]: r[\"n\"] for r in con.execute(\n708\t \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n709\t \" GROUP BY source\")}\n710\t last = {r[\"source\"]: r[\"ts\"] for r in con.execute(\n711\t \"SELECT source, MAX(ts) ts FROM sync_log WHERE ok=1 GROUP BY source\")}\n712\t con.close()\n713\t for s in registry:\n714\t s[\"active_listings\"] = counts.get(s[\"id\"], 0)\n715\t s[\"last_sync\"] = last.get(s[\"id\"])\n716\t return {\"sources\": registry}\n717\t\n718\t\n719\t# Villes de la grande région de Montréal (le reste de la province est\n720\t# compté dans « autres » — Outaouais, Estrie, Mauricie, Est-du-Québec…)\n721\t_VILLES_GM = (\n722\t \"Montréal\", \"Montréal-Est\", \"Laval\", \"Longueuil\", \"Brossard\", \"Terrebonne\",\n723\t \"Mascouche\", \"Repentigny\", \"Boucherville\", \"Saint-Lambert\", \"Westmount\",\n724\t \"Côte Saint-Luc\", \"Dorval\", \"Pointe-Claire\", \"Verdun\", \"LaSalle\", \"Anjou\",\n725\t \"Saint-Constant\", \"Delson\", \"Candiac\", \"Châteauguay\", \"Chambly\",\n726\t \"Sainte-Julie\", \"Varennes\", \"Belœil\", \"McMasterville\", \"Mont-Saint-Hilaire\",\n727\t \"Saint-Basile-le-Grand\", \"Saint-Bruno-de-Montarville\", \"Boisbriand\",\n728\t \"Richelieu\", \"Beauharnois\", \"Salaberry-de-Valleyfield\", \"Vaudreuil-Dorion\",\n729\t \"Kirkland\", \"Beaconsfield\", \"Dollard-des-Ormeaux\", \"Mont-Royal\", \"Outremont\",\n730\t)\n731\t\n732\t\n733\t@app.get(\"/api/stats\")\n734\tdef stats(syncs_since_h: float | None = None):\n735\t \"\"\"Stats globales + journal de synchronisation.\n736\t\n737\t `syncs_since_h` (optionnel) : fenêtre en heures pour `recent_syncs` — le\n738\t moniteur central (api-ka) polle aux 2 h et les 20 dernières entrées ne\n739\t couvrent que ~11 min d'activité sur ~200 sources (fausses alertes\n740\t « stale »). Avec le paramètre : toutes les entrées des N dernières heures\n741\t (max 4000) ; s'il y en a moins de 20, on retombe sur les 20 dernières\n742\t toutes époques confondues. Sans le paramètre : comportement historique\n743\t inchangé (LIMIT 20).\n744\t \"\"\"\n745\t con = db.connect()\n746\t gm = \",\".join(\"?\" * len(_VILLES_GM))\n747\t row = con.execute(\n748\t f\"\"\"SELECT COUNT(*) total,\n749\t SUM(CASE WHEN city='Québec' THEN 1 ELSE 0 END) quebec,\n750\t SUM(CASE WHEN city='Lévis' THEN 1 ELSE 0 END) levis,\n751\t SUM(CASE WHEN city IN ({gm}) THEN 1 ELSE 0 END) montreal,\n752\t SUM(CASE WHEN city NOT IN ('Québec','Lévis') AND city NOT IN ({gm})\n753\t THEN 1 ELSE 0 END) autres,\n754\t COUNT(DISTINCT source) sources,\n755\t AVG(price) avg_price\n756\t FROM listings WHERE active=1 AND published=1 AND dup_of IS NULL\"\"\",\n757\t list(_VILLES_GM) + list(_VILLES_GM)).fetchone()\n758\t log: list[dict] = []\n759\t if syncs_since_h is not None:\n760\t import time as _time\n761\t cutoff = _time.time() - syncs_since_h * 3600\n762\t log = [dict(r) for r in con.execute(\n763\t \"SELECT * FROM sync_log WHERE ts >= ? ORDER BY ts DESC LIMIT 4000\",\n764\t (cutoff,))]\n765\t if syncs_since_h is None or len(log) < 20:\n766\t log = [dict(r) for r in con.execute(\n767\t \"SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20\")]\n768\t # qualité des données : complétude moyenne + quarantaine (quality.py)\n769\t qual = con.execute(\n770\t \"\"\"SELECT ROUND(AVG(completeness),1) avg_completeness,\n771\t SUM(published=1) publiees, SUM(published=0) quarantaine\n772\t FROM listings WHERE active=1\"\"\"\n773\t ).fetchone()\n774\t con.close()\n775\t return {**dict(row), \"quality\": dict(qual), \"recent_syncs\": log}\n776\t\n777\t\n778\t@app.get(\"/api/stats/detailed\")\n779\tdef stats_detailed():\n780\t \"\"\"Agrégats du marché (source unique : louka/marketstats.py).\"\"\"\n781\t from . import marketstats\n782\t return marketstats.compute()\n783\t\n784\t\n785\t# --- Module Stats commun Groupe KA (contrat frontend/src/ka/stats/SPEC.md) ---\n786\t_PERIODES_VALIDES = {\"auj\", \"7j\", \"30j\", \"3m\", \"6m\", \"12m\", \"annee\", \"tout\"}\n787\t\n788\t\n789\t@app.get(\"/api/stats/dashboard\")\n790\tdef stats_dashboard(period: str = \"30j\",\n791\t from_: str | None = Query(None, alias=\"from\"),\n792\t to: str | None = None):\n793\t \"\"\"Tableau de bord analytique (KPI, séries, répartitions, tableaux…).\"\"\"\n794\t from . import statsdash\n795\t if period not in _PERIODES_VALIDES and not (from_ and to):\n796\t period = \"30j\"\n797\t return statsdash.compute(period, from_, to)\n798\t\n799\t\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e3961428-545e-41f7-9838-7ab09872b470","timestamp":"2026-08-24T21:45:34.322Z"}} {"type":"claude","event":{"type":"system","subtype":"task_progress","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","description":"Reading README.md","usage":{"total_tokens":85014,"tool_uses":49,"duration_ms":51000},"last_tool_name":"Read","uuid":"3984e285-8015-411b-bfa5-e2ad45741a9e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011CeNJhhxBSXzeQcGRKHjTz","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016ksZPbCK68km999gojqn43","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/README.md","limit":200},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":5737,"cache_read_input_tokens":79108,"cache_creation":{"ephemeral_5m_input_tokens":5737,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"65c08621-f08d-4c73-ad47-62301d2ec859"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016ksZPbCK68km999gojqn43","type":"tool_result","content":"1\t\n2\t\n3\t

\n4\t \"Lou·Ka\n5\t

\n6\t

Lou·Ka

\n7\t

Tous les logements à louer

\n8\t\n9\t
\n10\t\n11\t\"Site\"\n12\t\"Documentation\"\n13\t\"PDF\"\n14\t\"Nœud\"\n15\t\"Port\"\n16\t\"PM2\"\n17\t\n18\t
\n19\t\n20\t
\n21\t\n22\t\"Annonces\n23\t\"Sources\n24\t\"Villes\n25\t\"Régions\n26\t\"Loyer\n27\t\"Géolocalisées\n28\t\"Couverture\n29\t\"Séjours\n30\t\n31\t
\n32\t\n33\t
\n34\t\n35\t\"Python\"\n36\t\"FastAPI\"\n37\t\"React\"\n38\t\"SQLite\"\n39\t\"Mapbox\n40\t\"ngrok\"\n41\t\"Groupe\n42\t\"spbgit\"\n43\t\n44\t
\n45\t\n46\tChercher un appartement au Québec, c'est ouvrir des dizaines de sites 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** (synchronisation périodique + hash de contenu → ajouts, mises à jour et retraits automatiques ; une annonce disparue répond `410 Gone`). Chaque fiche renvoie vers l'annonce originale du gestionnaire via une passerelle de sortie transparente (`/passerelle/{uid}`).\n47\t\n48\tPour qui ? Les locataires qui cherchent (recherche filtrée, carte 3D, fiches complètes), les curieux du marché (observatoire des loyers, rapports PDF) et les gestionnaires (pages réclamables). **En chiffres (au 2026-08-24)** : 45 762 annonces actives publiées, 281 sources recensées, 250 connecteurs (221 location long terme + 29 court terme), 1 233 villes, 11 régions, 97,4 % des annonces géolocalisées, loyer médian 1 700 $ — plus **41 460 séjours court terme** (chalets, Airbnb…) dans la section `/court-terme`.\n49\t\n50\t## Visite guidée\n51\t\n52\t\n53\t \n54\t \n55\t \n56\t \n57\t \n58\t \n59\t \n60\t \n61\t \n62\t \n63\t \n64\t \n65\t \n66\t \n67\t \n68\t \n69\t \n70\t \n71\t \n72\t \n73\t \n74\t \n75\t \n76\t \n77\t \n78\t \n79\t \n80\t \n81\t \n82\t \n83\t \n84\t

Accueil — recherche et univers Lou·Ka (desktop)

Accueil — version mobile

Étape 1 · Cherchez depuis l'accueil

Étape 2 · Parcourez les résultats d'une ville

Étape 3 · Ouvrez la fiche d'un logement

Étape 4 · Suivez le marché sur /stats

Résultats de recherche — liste d'annonces filtrée

Vue carte 3D — clusters par prix moyen (ka-maps)

Fiche logement — photos, digest, badge marché, quartier

Registre des sources — chaque gestionnaire et sa synchro

Répertoire des villes (pages SEO)

Page ville — Québec

Observatoire du marché — loyers médians, KPI

Accueil — variante pleine page

Accueil mobile — variante pleine page
\n85\t\n86\t### App iOS native\n87\t\n88\t\n89\t \n90\t \n91\t \n92\t \n93\t \n94\t \n95\t \n96\t \n97\t \n98\t \n99\t

Accueil

Découverte

Carte

Fiche logement

Statistiques

Sources
\n100\t\n101\t## Fonctionnalités\n102\t\n103\t- **Recherche filtrée** — ville, quartier, type (3½, 4½…), loyer min/max, animaux, meublé, superficie, texte libre.\n104\t- **Vue carte 3D** — Lou-Ka Maps (framework `@groupe-ka/ka-maps`, moteur Mapbox GL JS) : clusters par prix moyen, « Rechercher dans cette zone », bascule 3D/2D, synchro liste ↔ carte.\n105\t- **Fiches complètes** — toutes les photos, digest structuré de la description (text mining regex FR, aucun LLM), badge marché, historique de prix (`price_log`), fiche PDF avec QR code.\n106\t- **Couches de données de quartier** — recensement 2021, registre des loyers (57 433 déclarations géocodées), inondation (BDZI), criminalité (SPVM, 12 mois par catégorie), qualité de l'air (RSQAQ/PST), prix de l'essence, commerces et transport en commun (métro/REM aux couleurs officielles), estimation Hydro-Québec live, **KA Scores** (95,5 % du parc couvert au 2026-08-24).\n107\t- **Location court terme** — section isolée `/court-terme` (sous-système `louka/shortterm/`, API `/api/ct/*`) : chalets, Airbnb (quadrillage carte quadtree limité à la frontière du Québec), Vrbo, Booking… 41 460 séjours, 22 sources actives sur 38 recensées, 27 régions (au 2026-08-24) ; SEO complet (JSON-LD `VacationRental`, sitemaps `sitemap-ct-N.xml`).\n108\t- **Pages villes SEO** — `/villes`, `/ville/{slug}`, `/ville/{slug}/{type}` en SSR léger, sitemaps dynamiques (chunks de 10 000), JSON-LD (`RealEstateListing` + `Offer` CAD), vrais `404`/`410`.\n109\t- **Observatoire du marché** — KPI temps réel, loyers médians par région/ville/taille, panneaux territoire (registre des loyers, population, essence), rapport de marché PDF multi-pages (`/api/stats/rapport.pdf`) et rapports PDF personnalisés (catalogue + `ReportBuilder`).\n110\t- **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` (281 entrées recensées).\n111\t- **Qualité éditoriale** — porte de pertinence (quarantaine des stationnements, garages, locaux commerciaux, locations à la nuit) : 59 668 annonces publiées / 1 175 en quarantaine, complétude moyenne 91,4 % (au 2026-08-24).\n112\t- **Comptes & SSO KA** — « Se connecter avec KA » via le hub groupe-ka.com (JWT HS256, `ka_id` émis par le hub) + Google OAuth direct ; favoris synchronisés vers « Mon univers Ka » ; pages gestionnaires réclamables (`/g/{source_id}`).\n113\t- **Widget KA Agent** — bulle de chat IA du Groupe KA intégrée au site (déplaçable, position mémorisée).\n114\t- **PWA installable** + app iOS SwiftUI 100 % native (dépôt séparé `lou-ka-ios` sur spbgit).\n115\t- **Robustesse** — garde-fou anti-dérive (`DRIFT_RATIO = 0.25`, `louka/db.py`) : si une source retourne soudainement beaucoup moins d'annonces, les retraits sont suspendus au lieu de vider l'inventaire ; chaîne de fetch anti-bot résiliente (Oxylabs → Scrapfly → Bright Data).\n116\t\n117\t## API (endpoints principaux)\n118\t\n119\tServis par `louka/web.py` (FastAPI) sur le port 8095.\n120\t\n121\t### Location long terme\n122\t\n123\t| Méthode | Endpoint | Rôle |\n124\t|---|---|---|\n125\t| `GET` | `/api/search` | recherche unifiée liste ↔ carte (mêmes filtres, même compteur) |\n126\t| `GET` | `/api/listings` · `/api/listings/{uid}` | annonces paginées + fiche détaillée (`410 Gone` si retirée) |\n127\t| `GET` | `/api/listings.geojson` | flux GeoJSON pour la carte 3D |\n128\t| `GET` | `/api/listings/{uid}/pdf` | fiche PDF avec QR code |\n129\t| `GET` | `/api/facets` · `/api/sources` | facettes de filtres, registre des sources |\n130\t| `GET` | `/api/fairvalue/{uid}` | badge marché (juste prix) d'une annonce |\n131\t| `GET` | `/api/rdl` · `/api/inondation` · `/api/air` · `/api/gaz` · `/api/commerces` · `/api/hydro` | couches de quartier : registre des loyers, BDZI, qualité de l'air, essence, commerces/transport, estimation Hydro-Québec |\n132\t| `GET` | `/api/kascores/stats` | statistiques du moteur KA Scores |\n133\t| `GET` | `/api/stats` · `/detailed` · `/dashboard` · `/report` · `/catalog` | observatoire : KPI, détail, tableau de bord, rapports |\n134\t| `POST` | `/api/stats/report/custom` | rapport PDF personnalisé (ReportBuilder) |\n135\t| `GET` | `/api/stats/rapport.pdf` | rapport de marché PDF multi-pages |\n136\t| `POST` | `/api/sync` | déclenchement d'une synchronisation |\n137\t| `GET` | `/doc/` | guide d'utilisation en ligne |\n138\t\n139\t### Location court terme (`/api/ct`, préfixe du sous-système `louka/shortterm/`)\n140\t\n141\t| Méthode | Endpoint | Rôle |\n142\t|---|---|---|\n143\t| `GET` | `/api/ct/listings` · `/api/ct/listings/{uid}` | séjours paginés + fiche détaillée |\n144\t| `GET` | `/api/ct/listings.geojson` | flux GeoJSON de la carte court terme |\n145\t| `GET` | `/api/ct/facets` · `/api/ct/sources` | facettes, registre des sources court terme |\n146\t| `GET` | `/api/ct/stats` | KPI court terme (total, sources, régions, prix/nuit) |\n147\t| `POST` | `/api/ct/sync` | déclenchement d'une synchronisation court terme |\n148\t\n149\t## Architecture\n150\t\n151\tPipeline : **connecteurs → normalisation → déduplication → SQLite → API/SSR/frontend**.\n152\t\n153\t- **Backend Python 3.14 / FastAPI / Uvicorn** (`louka/web.py`) : API JSON + service du build Vite + SSR SEO (`louka/seo.py`).\n154\t- **SQLite (WAL)** (`louka/db.py`) : migrations auto, upsert par hash de contenu, anti-dérive ; bases annexes dans `data/` (quartier, air, gaz, hydro, commerces, inondation…).\n155\t- **250 connecteurs** auto-découverts : 221 long terme (`louka/connectors/`) + 29 court terme (`louka/shortterm/connectors/`), extraction directe / Firecrawl / Scrapfly / Bright Data / acteurs Apify maison selon la source.\n156\t- **Enrichissement** : géocodage (Nominatim + Adresses Québec), POI OSM, stats de quartier, KA Scores, estimation Hydro-Québec.\n157\t- **Frontend React 18 + Vite + TypeScript** (`frontend/`), cartographie `@groupe-ka/ka-maps` (Mapbox GL).\n158\t- **CLI `run.py`** : `sync` · `watch` · `sync-ct` · `watch-ct` · `serve` · `geocode` · `quality` · `imgaudit` · `fairvalue` · `poi` · `quartier` · `kascores` · `rdl` · `air-refresh` · `gaz-refresh` · `hydro-precompute` · `record` (fixtures HTTP pour tests hors-ligne).\n159\t\n160\tProcessus PM2 sur le nœud :\n161\t\n162\t| Processus | Commande réelle | Rôle | Cadence |\n163\t|---|---|---|---|\n164\t| `lou-ka-web` | `.venv/bin/python run.py serve 8095` | serveur FastAPI/Uvicorn (API + frontend + SSR) sur le port **8095** | continu |\n165\t| `lou-ka-sync` | `.venv/bin/python run.py watch 60` | boucle de resynchronisation des connecteurs long terme | cycle de 60 min |\n166\t| `lou-ka-ngrok` | `ngrok http --url=www.lou-ka.com 8095` | tunnel ngrok vers **www.lou-ka.com** | continu |\n167\t\n168\tLa section court terme se resynchronise via `run.py watch-ct` (défaut 360 min) ou à la demande (`run.py sync-ct`, `POST /api/ct/sync`).\n169\t\n170\t## Démarrage rapide\n171\t\n172\t```bash\n173\t# Backend (Python 3.14)\n174\tpython3 -m venv .venv && .venv/bin/pip install -r requirements.txt\n175\t\n176\t# Frontend (nécessite le framework ka-maps bâti à côté : ../ka-maps)\n177\tcd frontend && npm install && npm run build && cd ..\n178\t\n179\t.venv/bin/python run.py sync # ingestion (toutes les sources ou une liste)\n180\t.venv/bin/python run.py sync-ct # ingestion court terme\n181\t.venv/bin/python run.py serve 8095 # API + frontend sur http://localhost:8095\n182\t.venv/bin/python run.py watch 60 # resynchronisation en boucle (minutes)\n183\t\n184\t# Tests (pytest + fixtures HTTP rejouables hors-ligne)\n185\t.venv/bin/python -m pytest tests/\n186\t```\n187\t\n188\tEn développement frontend : `cd frontend && npm run dev` (Vite sur `:5173`, proxy `/api`).\n189\t\n190\t## Variables d'environnement\n191\t\n192\tAucun secret dans le repo — tout passe par l'environnement du nœud. Noms attendus :\n193\t\n194\t| Variable | Rôle |\n195\t|---|---|\n196\t| `SCRAPFLY_KEY` · `FIRECRAWL_API_KEY` · `APIFY_TOKEN` | extraction anti-bot / rendu / acteurs Apify maison |\n197\t| `BRIGHTDATA_API_KEY` · `BRIGHTDATA_ZONE` | Web Unlocker Bright Data (repli anti-bot) |\n198\t| `OXYLABS_PROXY` · `OXYLABS_PROXY_USER` · `OXYLABS_PROXY_PASS` | proxies résidentiels (1er maillon de la chaîne de fetch) |\n199\t| `GOOGLE_CLIENT_ID` · `GOOGLE_CLIENT_SECRET` | connexion Google OAuth directe |\n200\t| `KA_SSO_SECRET` · `KA_HUB_URL` · `SESSION_SECRET` | SSO KA ID (JWT HS256 partagé avec groupe-ka.com) + sessions |\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n"}]},"parent_tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b1beb1ef-7f8d-4fa3-8689-a7bda626a17b","timestamp":"2026-08-24T21:45:36.300Z"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"a57e70d144471fbcc","tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","status":"completed","output_file":"","summary":"Audit complet du repo lou-ka","usage":{"total_tokens":91883,"tool_uses":49,"duration_ms":210410},"uuid":"cc1ee093-90a7-4a6d-bb73-a6e044931fa4","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PVAkMEvDYcgc8vxwSNQ8ci","type":"tool_result","content":[{"type":"text","text":"Excellent. Maintenant j'ai une excellente compréhension. Générons un rapport complet et détaillé.\n\n---\n\n## RAPPORT AUDIT APPROFONDI — LOU-KA (Agrégateur Québécois de Logements)\n\n### 1. STACK TECHNOLOGIQUE\n\n#### Backend\n- **Langage/Framework** : Python 3.14 + **FastAPI** (ligne 24 de `web.py`)\n- **Serveur HTTP** : **Uvicorn** (démarrage via `uvicorn.run()` ligne 131 de `run.py`)\n- **Port** : **8095** (mentionné README ligne 119, configuration PM2)\n- **Exécution** : Mode production via **PM2** (processus `lou-ka-web` exécutant `python run.py serve 8095`), plus synchronisation périodique (`lou-ka-sync` avec `python run.py watch 60` = 60 min, ligne 42 de `run.py`)\n\n#### Base de données\n- **Système** : **SQLite** avec **WAL** (Write-Ahead Logging — ligne 241-242 de `db.py`, `PRAGMA journal_mode=WAL`)\n- **Chemin** : `/Users/simon-pierreboucher/apps/lou-ka/data/louka.db` (défini ligne 18 de `db.py`, soit `DB_PATH`)\n- **ORM** : **SQL brut** (construction manuelle des requêtes SQL via `.execute()`, pas d'ORM comme SQLAlchemy)\n- **Migrations** : **Automatiques à la connexion** (fonction `connect()` ligne 221-243 de `db.py` exécute `CREATE TABLE IF NOT EXISTS` + `ALTER TABLE` pour colonnes manquantes)\n- **Taille actuelle** : **~1,5 Go** (`louka.db-wal` = 28 Mo WAL, `louka.db` = 1,47 Go, état au 2026-08-24)\n\n#### Frontend\n- **Framework** : **React 18.3.1** + **TypeScript 5.5.4**\n- **Build/Dev** : **Vite 5.4.0** (fichier `vite.config.ts`, scripts `npm run dev`, `npm run build`)\n- **Routage** : **React Router v6.26** (`react-router-dom`)\n- **Cartographie** : **Mapbox GL 3.28.1** + wrapper interne **`@groupe-ka/ka-maps`** (dépendance pointant vers `../../ka-maps`)\n- **Build output** : `/frontend/dist/` (SSR intégré côté backend via `louka/seo.py`)\n\n#### Persistance des enrichissements (bases annexes)\nToutes dans `/data/` :\n- `quartier.db` (120 Mo) : aires de diffusion 2021, stats de quartier, démographie, crimes\n- `tal.db` (20 Ko) : historique Tribunal administratif du logement (SOQUIJ)\n- `rdl.db` (15 Mo) : Registre des loyers (déclarations publiques)\n- `hydro.db` (32 Ko) : cache des estimations électricité Hydro-Québec\n- `air.db` (65 Ko) : qualité de l'air (RSQAQ/PST)\n- `gaz.db` (454 Ko) : stations-service (gazquebec.ca)\n- `inondation.db` (493 Mo) : zones inondables (BDZI gouvernement du Québec)\n- `commerces.db` (106 Ko) : cache POI commerces\n- `transit.db` (3,7 Mo) : arrêts et lignes transport en commun\n\n---\n\n### 2. MODÈLE DE DONNÉES (Schéma Complet)\n\n#### Table `listings` (annonces locatives)\n**Colonnes clés** (définies en détail aux lignes 31-177 de `db.py`, _SCHEMA + _MIGRATIONS) :\n\n| Colonne | Type | Rôle | Etat |\n|---------|------|------|------|\n| `uid` | TEXT PK | `{source}:{external_id}` — clé unique | Primaire |\n| `source` | TEXT NOT NULL | ID connecteur (ex. \"msi\", \"logisco\") | Indexée |\n| `external_id` | TEXT NOT NULL | Identifiant chez la source | — |\n| `url` | TEXT | URL annonce originale | — |\n| `title`, `address`, `sector`, `city` | TEXT | Libellés d'affichage normalisés | — |\n| `unit_type` | TEXT | \"1½\", \"2½\", \"3½\"… \"Studio\", \"Chambre\" (schéma.py) | — |\n| `bedrooms`, `bathrooms` | REAL | Nombre chambre fermées/salles bain | Nullable |\n| `price` | REAL | Loyer mensuel $/mois | Nullable (si absent = quarantaine) |\n| `price_label` | TEXT | Texte original (\"à partir de 799$\") | — |\n| `availability`, `availability_date` | TEXT | Texte + ISO \"2026-07-01\" ou \"now\" | — |\n| `area_sqft` | REAL | Superficie pi² | Nullable |\n| `pets`, `furnished` | TEXT, INT | \"oui\"/\"non\"/\"conditions\", boolean | Nullable |\n| `description` | TEXT | Corps brut | — |\n| `digest` | TEXT (JSON) | Description structurée (textmine.py) | — |\n| `amenities` | TEXT (JSON) | Liste commodités sources | — |\n| `details` | TEXT (JSON) | Champs structurés : `{\"inclusions\": {\"heating\": true}, \"parking\": {...}}` | — |\n| `images` | TEXT (JSON) | URLs photos brutes | — |\n| `images_ok` | TEXT (JSON) | Galerie nettoyée (imgcheck.py) — priorité sur `images` | Nullable |\n| `img_audit` | TEXT (JSON) | Audit de qualité des images | Nullable |\n| `lat`, `lng` | REAL | Coordonnées géocodées | Nullable, indexées (`idx_listings_geo`) |\n| `coord_key` | TEXT | Clé immeuble \"lat,lng\" (4 déc ≈ 11 m) — jointure KA Scores | Indexée |\n| `geocode_failed` | INT | Flag si géocodage échoué définitif | Default 0 |\n| `dauid` | TEXT | Aire de diffusion 2021 StatCan — jointure quartier.db | Nullable |\n| `content_hash` | TEXT | SHA256 du contenu (détection changements) | — |\n| `first_seen`, `last_seen`, `updated_at` | REAL (timestamp unix) | Timestamps | — |\n| `active`, `miss_count` | INT | active=0 après délai de grâce (MISS_GRACE=2), miss_count incrémenté | Indexée (`idx_listings_active`) |\n| `published`, `completeness`, `quality_reasons` | INT, REAL, JSON | Score 0-100, seuil PUBLISH_THRESHOLD=40, raisons quarantaine | — |\n| `dup_of`, `dup_sources` | TEXT, JSON | Déduplication inter-sources (dedup.py) | Nullable |\n\n**Indexes** :\n- `uid` (PRIMARY KEY)\n- `idx_listings_source` (source)\n- `idx_listings_city` (city)\n- `idx_listings_active` (active, published)\n- `idx_listings_geo` (lat, lng)\n- `idx_listings_pub` (active, published)\n- `idx_listings_coord` (coord_key)\n\n**Taille** : **60 999 listings actives** (active=1), **59 819 publiées** (published=1 ET active=1), **1 180 en quarantaine** — au 2026-08-24\n\n#### Table `price_log` (historique des prix)\n```sql\nCREATE TABLE price_log (\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL, -- timestamp insertion\n price REAL -- NULL = retrait de l'affichage\n);\n```\n- **Taille** : 95 767 entrées = 91 302 UIDs distincts\n- **Rôle** : Suivi mensuel/historique (affiché sur fiche : \"prix_history\" — 6 derniers en ligne 656 de `web.py`)\n- **Insertion** : Automatique lors du sync (ligne 356-358, 382-384 de `db.py`)\n\n#### Table `fairvalue` (juste valeur locative)\n```sql\nCREATE TABLE fairvalue (\n uid TEXT PRIMARY KEY, -- listings.uid\n fv REAL, fv_low REAL, fv_high REAL, -- estimation + fourchette p25/p75\n deviation REAL, -- (prix - fv) / fv\n verdict TEXT, -- \"sous\" | \"marche\" | \"sur\" | NULL\n confidence TEXT, -- \"fort\" | \"moyen\" | \"faible\"\n method TEXT, -- N1 | N2 | N3 (voir fairvalue.py)\n comps INTEGER, -- nombre de comparables (kNN)\n segment TEXT, -- clé segment {ville}/({quartier}) × bedrooms\n model_version TEXT, -- \"fv-logement-1.0\" (ligne 29 de fairvalue.py)\n computed_at REAL\n);\n```\n- **Taille** : 45 709 entrées (74 % du parc)\n- **Rôle** : Badge marché \"Bon deal\" / \"Dans le marché\" / \"Au-dessus\" sur les listings\n- **Algorithme** (fairvalue.py) :\n - **N1** : Segment de référence (ville × quartier × bedrooms) = médiane + p25/p75\n - **N2** : Ajustements multiplicatifs par features (meublé, chauffage, parking)\n - **N3** : Comparables kNN géographiques (k=12, rayon 3 km, pondération gaussienne)\n - Combinaison + couloir de sécurité (70 %-130 % fourchette p25/p75)\n\n#### Table `kascores` (scores maison 0-100 par immeuble)\n```sql\nCREATE TABLE kascores (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi 4 déc\n lat REAL, lng REAL,\n walk REAL, -- marchabilité besoins quotidiens\n transit REAL, -- desserte transport collectif\n bike REAL, -- praticité vélo\n calme REAL, -- tranquillité estimée\n services REAL, -- richesse services (1 km / 3 km)\n global REAL, -- moyenne pondérée (customisable côté client)\n details TEXT (JSON), -- détail par score\n version TEXT, -- \"2026.08-v1\" (ligne 38 de kascores.py)\n computed_at REAL\n);\n```\n- **Taille** : 28 438 immeubles (coord_key distincts)\n- **Couverture** : 95,5 % du parc (cf. README ligne 106)\n- **Sources de données** :\n - OpenStreetMap via Overpass (POI, routes, rails, cyclable, bars)\n - StatCan PMD 2021 (fréquence/qualité transport collectif)\n - Aucune donnée inventée : secteur sans données → NULL\n- **Barème** (kascores.py, lignes 43-66) :\n - Walk : épicerie/pharmacie/parc/café/école/clinique/garderie/dépanneur/gym/bibliothèque\n - Transit : arrêt bus (≤600m), métro (≤1500m), PMD percentile\n - Bike : km cyclables dans 1 km\n - Calme : distance routes/rails/aéro/industriel (pénalités), bonus parcs\n - Services : familles (commerces, santé, education, loisirs) par rayon + saturation\n\n#### Tables de cache spatial\n```sql\nCREATE TABLE poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" 4 déc\n pois TEXT (JSON), -- [{\"cat\": \"epicerie\", \"name\": \"Maxi\", \"dist_m\": 450}, ...]\n ts REAL\n);\n\nCREATE TABLE geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (normalize.py)\n lat REAL, lng REAL,\n provider TEXT, -- \"nominatim\" | \"adresses-quebec\"\n failed INT DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE env_tiles (\n tile_key TEXT PRIMARY KEY, -- \"ty,tx\" tuiles 0.5°\n data TEXT (JSON), -- routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n```\n\n#### Tables de contrôle qualité\n```sql\nCREATE TABLE image_checks (\n url TEXT PRIMARY KEY,\n status TEXT, -- \"ok\" | \"dead\" | \"small\" | \"error\"\n width INTEGER, height INTEGER, bytes INTEGER,\n sha1 TEXT, -- hash détection doublons/placeholders\n checked_at REAL\n);\n\nCREATE TABLE detail_cache (\n source TEXT, external_id TEXT,\n key TEXT, -- hash liste source\n payload TEXT (JSON), -- payload détail si clé inchangée\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n```\n\n#### Tables utilisateurs et favoris\n```sql\nCREATE TABLE users (\n id INT PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID \"sub\")\n ka_id TEXT, -- Groupe KA \"ka-0123456789\"\n email TEXT,\n name TEXT, picture TEXT, -- avatar Google\n public INT DEFAULT 0, -- profil public /u/{ka_id}\n role TEXT, -- \"locataire\" | \"gestionnaire\" | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n display_name TEXT, avatar_url TEXT, -- profil custom\n bio TEXT, city TEXT, phone TEXT, website TEXT,\n socials TEXT (JSON), -- {instagram, facebook, x, linkedin…}\n created_at REAL, last_login REAL\n);\n\nCREATE TABLE favorites (\n user_id INT NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n```\n\n#### Tables administratives\n```sql\nCREATE TABLE sync_log (\n id INT PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INT, added INT, updated INT, removed INT,\n ok INT, -- 1 = succès, 0 = erreur\n message TEXT, -- \"ok\" | alerte dérive | message erreur\n stats TEXT (JSON) -- {\"null_price_rate\": 0.05, \"alert\": \"…\"}\n);\n\nCREATE TABLE source_profiles (\n source_id TEXT PRIMARY KEY, -- data/sources.json[].id\n owner_user_id INT,\n tagline TEXT, description TEXT,\n website TEXT, phone TEXT, email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE fv_segments ( -- (pré-calculée, pas insérée directement)\n segment TEXT PRIMARY KEY, -- \"quebec/beauport|2\" = clé N1\n median REAL, p25 REAL, p75 REAL,\n count INT, -- volume du segment\n updated_at REAL\n);\n```\n\n#### Tables court terme (louka/shortterm/)\nSchéma similaire dans `data/louka_ct.db` (238 Mo) — voir `louka/shortterm/schema.py` + `louka/shortterm/db.py`\n\n---\n\n### 3. CRAWLERS/CONNECTEURS ET PIPELINE D'INGESTION\n\n#### Registre des connecteurs\n- **Découverte auto** : `louka/connectors/__init__.py` (lignes 9-32) — tout module contenant une classe héritant de `BaseConnector` avec `source_id` non vide est enregistré automatiquement\n- **Base de classe** : `louka/connectors/base.py` (interface standardisée)\n- **Nombre total** : **250 connecteurs** (221 long terme + 29 court terme)\n - 281 sources recensées dans `data/sources.json` (certaines hors-ligne ou fusionnées)\n\n#### Connecteurs long terme (louka/connectors/)\nExemples d'architectures variées :\n- **Directes (HTML/API)** : `msi.py`, `logisco.py`, `laberge.py`, `cogir.py` — sites gérés directement\n- **Via Firecrawl** : extraction JavaScript/SPA (site clé : `FIRECRAWL_API_KEY` dans `.env`)\n- **Via Scrapfly** : rendu JS + anti-bot (site clé : `SCRAPFLY_KEY`, utilisé aussi pour TAL)\n- **Via Bright Data Web Unlocker** : `apartments_com.py` (Akamai/challenges, site clé `BRIGHTDATA_API_KEY`)\n- **Via Oxylabs + proxy résidentiel** : chaîne résiliente Oxylabs → Scrapfly → Bright Data (see `hydro.py` ligne 54-64)\n- **Acteurs Apify maison** : `ka-fb-marketplace` (Facebook Marketplace) + `ka-apartments-com` (Apartments.com via Apify + Oxylabs)\n\nExemples de connecteurs :\n- `lafrance_mathieu.py`, `msi.py`, `logisco.py`, `laberge.py`, `cogir.py` — gestionnaires Québec/Lévis\n- `kijiji.py`, `lespac.py`, `fb_marketplace.py` — petites annonces / classifiés\n- `logisquebec.py`, `louer_ca.py`, `rentals_ca.py` — portails agrégateurs\n- `zumper.py`, `apartments_com.py` — portails étrangers (contenu pancanadien)\n- `rémax_quebec.py`, `royal_lepage.py`, `sutton.py` — courtiers immobiliers\n\n#### Pipeline d'ingestion (`louka/ingest.py`)\n**Fonction `run(sources: list | None)`** (ligne 39) :\n1. Boucle sur chaque connecteur (auto-découverts sauf si liste ciblée)\n2. **Circuit breaker** (lignes 16-23, 50-57) : source qui a échoué aux BREAKER_FAILS=5 passages consécutifs est sautée, retentée tous les BREAKER_RETRY_EVERY=5 passages (lissage des timeouts)\n3. Appel `cls().fetch()` pour chaque connecteur → retourne liste `Listing` (schéma.py)\n4. Finalisation `lst.finalize()` (ligne 66) : normalisation commune\n5. Synchronisation `db.sync_source(con, source, listings)` (ligne 69)\n\n**Synchronisation (`db.sync_source()`, lignes 279-427 de db.py)** :\n- **Nouveau** : insertion complète + entrée initiale dans `price_log`\n- **Modifié** (content_hash changé) : UPDATE avec new hash, images_ok/img_audit invalidées, entrée `price_log` si prix changé\n- **Inchangé** (hash identique) : UPDATE `last_seen`, reset `miss_count`\n- **Absent** (source n'a pas retourné) : incrémente `miss_count`, au passage MISS_GRACE=2 : `active=0`\n- **Dérive détectée** (lignes 250-276) : si volume ≤ 25 % de médiane 5 derniers passages, retraits suspendus → alerte dans `sync_log`\n\n**Qualité lors du sync** (lignes 306-337) : score de complétude + publication/quarantaine automatique (quality.py)\n\n#### Normalisation et déduplication\n\n**Normalisation (`louka/normalize.py`)**\n- `clean_address()`, `clean_city()`, `clean_sector()` — suppression accents, normes\n- `parse_price(price_label)` — extraction numérique\n- `parse_availability_date(availability)` — normalisé en ISO ou \"now\"\n- `parse_area_sqft(texte)` — recherche mention superficie\n- `parse_bedrooms()`, `parse_bathrooms()` — extraction texte ou type d'unité\n- `extract_details()`, `merge_details()` — champs structurés (inclusions, parking, appliances)\n\n**Finalisation (`Listing.finalize()`, lignes 84-159 de schema.py)**\n- Validation géographique : coordonnées source rejetées si hors province (44.5-63° N, -80° à -56° O)\n- Description structurée : appel `textmine.analyser()` si digeste absent\n- Remplissage chambre/salle bain par cascade : champ explicite → details structurés → type unité → extraction texte\n\n**Déduplication inter-sources (`louka/dedup.py`, récalcul complet à chaque passe)**\n1. **Regroupement** par proximité (grille lat/lng ≈110 m) OU clé adresse normalisée (civique + rue + ville sans types)\n2. **Confirmation** : même type unité (si connu) + prix ±4 % + signal géographique/adresse\n3. **Résolution** : source **la plus autoritaire** devient canonique (PORTALS=10, CLASSIFIEDS=20, gestionnaires=0)\n4. **Marquage** : `dup_of = uid_canonique`, `dup_sources = [source2, source3, …]` (badge \"aussi publiée sur…\")\n5. **Affichage** : web.py filtre `WHERE dup_of IS NULL`\n\n#### Quality control (`louka/quality.py`)\n**Fonction `evaluate(d: dict)`** (ligne 152) retourne (score, publiable, raisons) :\n\n| Critère | Poids | Calcul |\n|---------|-------|--------|\n| Prix 175-20 000 $/mois | 20 | 0 si absent, 20 sinon |\n| Adresse OU coords | 14 | 14 si adresse/coords, 7 si secteur seulement |\n| Ville | 4 | — |\n| Description ≥200 car | 14 | 14 si ≥200, 7 si ≥60 |\n| Images | 16 | 0→0, 1→6, 2-4→11, ≥5→16 |\n| Type d'unité ou bedrooms | 8 | — |\n| Disponibilité | 5 | — |\n| Superficie | 4 | — |\n| Salles de bain | 3 | — |\n| Commodités ≥3 | 5 | 5 si ≥3, 2,5 si ≥1 |\n| Details ≥3 champs | 4 | 4 si ≥3, 2 si ≥1 |\n| URL | 3 | — |\n\n**Portes dures** : annonce rejetée (published=0) si :\n- Pas de prix ou prix non plausible\n- Pas de localisation (adresse + coords + ville)\n- Pas de contenu exploitable (desc<40 car, pas commodités, pas type unité)\n- Pertinence manquante (voir fonction `relevance_reason()`, lignes 82-125)\n\n**Pertinence** (`relevance_reason()`) : quarantaine automatique pour :\n- Stationnement/garage (titre/description, prix <700$)\n- Local commercial/bureau\n- Location à la nuit (<600$, mention \"par nuit\")\n- Objets non résidentiels (prix <300$, aucun vocabulaire logement)\n\n**Seuil** : PUBLISH_THRESHOLD=40 — score <40 + aucune porte dure = publiée; score <40 + porte dure = quarantaine\n\n**Backfill** (`quality.backfill()`, ligne 240) : recalcul complet sur tous les actifs\n\n#### Historique de prix (`louka/db.py` + `web.py`)\n- Entrée `price_log` à chaque changement de prix ou retrait\n- Affichage fiche : 6 dernières (ligne 656 web.py)\n- Format : `[{ts: unix_timestamp, price: float | null}]`\n\n---\n\n### 4. GÉNÉRATION DES FICHES DE LOGEMENT\n\n#### Architecture de la fiche\n**Frontend** : `frontend/src/pages/Listing.tsx` (React, 800+ lignes) — SSR + client-side hydration\n**Backend** : `louka/web.py` endpoint `/api/listings/{uid}` (lignes 613-661) — données brutes\n**Backend API** : endpoints spécialisés pour composants (fairvalue, poi, quartier, TAL, RDL, air, hydro, gaz, commerces)\n\n#### Ordre des sections (standard Groupe KA, doc ligne 6-8 de Listing.tsx) :\n1. **Galerie** (lightbox fullscreen + balayage + pincement-zoom)\n2. **Prix + badges**\n - Loyer + \"à partir de\" si applicable\n - Badge marché (juste valeur) : \"Bon deal\" / \"Dans le marché\" / \"Au-dessus\"\n - Badge loyer vs secteur : `±X % vs le secteur`\n3. **Adresse + chips**\n - Adresse + coordonnées + secteur/ville\n - Badges commodités confirmées : \"Chauffage inclus\", \"Électricité incluse\", \"Laveuse-sécheuse\", \"Climatisation\", etc.\n4. **CTA + PDF** : lien vers annonce originale + bouton \"Télécharger fiche PDF\"\n5. **Description structurée** (digest)\n6. **Inclusions + détails pratiques** (parking, animaux, furnished, etc.)\n7. **Analyse de prix** (juste valeur : segment, comparables, confiance)\n8. **Emplacement** (mini-carte 3D Mapbox)\n9. **KA Scores** (Walk/Transit/Bike/Calme/Services + global avec justifications)\n10. **Quartier** (démographie, proximité, défavorisation, chaleur, criminalité)\n11. **À proximité** (commodités OSM groupées)\n12. **Enrichissements complémentaires**\n - Historique TAL (tribunal du logement)\n - Registre des loyers (déclarations communautaires)\n - Risque d'inondation (BDZI)\n - Qualité de l'air (RSQAQ)\n - Stations-service (gazquebec.ca)\n - Estimation coût électricité (Hydro-Québec)\n\n#### Composants réutilisables (frontend/src/components/)\n- `ListingCard.tsx` (640 px miniature, liste)\n- `SmartImg.tsx` (lazy-loading + fallback)\n- `FairValueBadge.tsx` (badge verdict + pourcentage)\n- `PriceAnalysis.tsx` (segment, comparables, distribution)\n- `KaScoresBlock.tsx` (5 pastilles 0-100 + détails)\n- `QuartierBlock.tsx` (démographie + proximité + criminalité + chaleur)\n- `HistoriqueTAL.tsx` (bloc décisions SOQUIJ)\n- `RegistreLoyers.tsx` (listes loyers déclarés autour)\n- `RisqueInondation.tsx` (zones BDZI)\n- `QualiteAir.tsx` (station RSQAQ + indice)\n- `EssenceProche.tsx` (prix courant stations gazquebec)\n- `CommercesProches.tsx` (Mapbox + POI groupés)\n- `HydroEstimation.tsx` (coût annuel estimé)\n- `ListingMap3D.tsx` (Mapbox GL lazy-loaded)\n- `MetroLignes.tsx` (lignes REM/métro Montréal)\n\n#### PDF (louka/pdfgen.py)\n**Fonction `fiche_pdf(uid)`** (généré à la demande `/api/listings/{uid}/pdf`) :\n- Généré avec **ReportLab** (lib Python PDF)\n- Palette Lou-Ka : blanc #ffffff, marine #0b1330, orange #ff6a00\n- Contenu : photos (jusqu'à 4 en grille), prix + analyse, chips, description structurée, inclusions, KA Scores, quartier, POI, QR code vers la fiche web\n- Taille page : Letter (8.5 × 11 in)\n\n**Rapport global** (`rapport_pdf()`) : agrégats du marché (totaux, régions, histogramme loyers, types, villes, gestionnaires)\n\n#### SSR (Server-Side Rendering)\n**Fichier** : `louka/seo.py` (1030 lignes)\n- Meta tags OpenGraph (og:image, og:title, og:description)\n- JSON-LD `RealEstateListing` + `Offer` (CAD)\n- Sitemaps dynamiques XML (chunks 10 000 URL)\n- Rendu HTML head côté serveur + body hydraté React client-side\n\n---\n\n### 5. ENRICHISSEMENTS EXISTANTS\n\n#### 5.1 **Géocodage d'adresses** (`louka/geocode.py`)\n- **Primaire** : Nominatim (OpenStreetMap, gratuit)\n- **Secondaire** : Adresses Québec (MERN — rues récentes, développements Lévis)\n- **Cache** : `geocode_cache` table (une adresse normalisée n'est géocodée qu'une fois par immeuble)\n- **Politesse** : 1 req/s (REQUEST_DELAY=1.1 sec)\n- **Validation** : coordonnées sources rejetées si hors province (bounding boxes par ville)\n- **Fonction** : `run_batch()` (ligne 59 de run.py) — traitement par lots Adresses Québec\n- **Erreurs** : flag `geocode_failed=1` si échouée → retry après 30 jours\n\n#### 5.2 **Commodités de proximité (POI)** (`louka/poi.py`)\n- **Source** : Overpass API (OpenStreetMap POI)\n- **Catégories** (54 de poi.py) : épicerie, dépanneur, pharmacie, école, garderie, parc, bus, métro, gym, café, clinique, hôpital, bibliothèque\n- **Stratégie tuiles** : téléchargement par tuile 0.5° (pas 1 req/immeuble coûteuse)\n- **Calcul** : haversine local, retenue du plus proche par catégorie\n- **Cache** : `poi_cache` par coord_key (4 déc = ±11 m, partage entre unités immeuble)\n- **Refresh** : tous les ~3 mois (REFRESH_AFTER=90 days)\n- **Affichage fiche** : groupes repliables (Courses, Transport, Études, Santé, Vie de quartier)\n\n#### 5.3 **Juste valeur locative (Fair Value)** (`louka/fairvalue.py`)\n- **Modèle** : `fv-logement-1.0` (versionnage = recalcul parc entier si changement)\n- **Trois niveaux** :\n - **N1** : Segment {ville/quartier} × bedrooms → médiane + p25/p75 (min 8 annonces)\n - **N2** : Ajustements multiplicatifs (meublé, chauffage, électricité, parking, climatisation, balcon, ascenseur, piscine, lave-vaisselle)\n - **N3** : Comparables kNN géographiques (k=12, rayon 3 km, pondération gaussienne exp(-(d/800m)²))\n- **Combination** : N1 + N3 weighted (poids kNN 0.6 si disponible)\n- **Fourchette de sécurité** : 70 % - 130 % fourchette p25/p75\n- **Verdict** : \"sous\" (<-8 %), \"marche\" (-8 % à +8 %), \"sur\" (>+8 %)\n- **Confiance** : \"fort\" / \"moyen\" / \"faible\" selon volume segment + kNN\n- **Recalcul** : automatique après dédup/geocode (ingest.py ligne 128-131)\n- **Stockage** : table `fairvalue` + table pré-calculée `fv_segments` (jointe en requête SQL)\n\n#### 5.4 **Analyse de quartier** (`louka/quartier.py`)\nSource unique : **`data/quartier.db`** (base statique construite par scripts — voir docs) :\n\n| Bloc | Données | Source |\n|------|---------|--------|\n| **Démographie** | population, densité, âge médian, revenu médian, % locataires, loyer moyen, % francophone, % universitaire | Recensement 2021 StatCan (DA_STATS) |\n| **Proximité** (PMD) | scores 0..1 de proximité à ressources (épiceries, pharmacies, écoles, parcs, transport) | StatCan Proximity Measures 2021 |\n| **Défavorisation** | quintiles matériel/social | INSPQ (Institut national santé publique) |\n| **Îlots de chaleur** | classe 1-9 (fraîcheur à chaleur) + écart temp | INSPQ (données liées immeuble via lat/lng) |\n| **Criminalité** | Montréal SPVM : points 12 mois + 12 précédents, par catégorie | SPVM (police Montréal) |\n| **Criminalité** | Autres villes : indice de gravité criminelle (IGC) | Services policiers locaux (Québec, Lévis, Laval, Longueuil) |\n\n**Jointure** : `da_poly` (point-dans-polygone par lat/lng) → DAUID (aire de diffusion 2021) → croisement tables stats\n\n**Stockage** : colonne `dauid` dans listings + références dans quartier.db\n\n#### 5.5 **Registre des loyers** (`louka/rdl.py`)\n- **API** : registre-des-loyers.ca (API JSON ouverte)\n- **Données** : 57 433 déclarations géocodées (vol d'oiseau, locataires volontaires)\n- **Affichage** : bloc \"Registre des loyers\" fiche + `/api/rdl?lat=…&lng=…` (rayon par défaut 600 m)\n- **Stockage** : table `rdl_housings` (base séparée `data/rdl.db`)\n- **Calcul** : médiane + écart-type par rayon autour de l'annonce\n\n#### 5.6 **Historique Tribunal administratif du logement (TAL)** (`louka/tal.py`)\n- **Source** : SOQUIJ (citoyens.soquij.qc.ca, tribunal 35)\n- **Stratégie** : Recherche « no civique rue » via Scrapfly (JS rendering + IP résidentielle), vérification adresse exacte dans texte intégral\n- **Cache** : table `tal_lookup` + `tal_decisions` dans `data/tal.db` (base séparée)\n- **Refresh** : révérification après ~6 mois (REFRESH_DAYS=180)\n- **Affichage** : bloc \"Historique TAL\" fiche (décisions par demandeur/type : non-paiement, résiliation, expulsion, reprise…)\n- **Stockage** : clé adresse normalisée (civique + rue + ville)\n\n#### 5.7 **Inondation** (`louka/inondation.py`)\n- **Source** : BDZI (Base de données des zones inondables, gouvernement du Québec)\n- **Stockage** : `data/inondation.db` (493 Mo — raster/tuiles)\n- **Lookup** : par lat/lng → classe de risque (récurence)\n- **Affichage** : bloc \"Risque d'inondation\" fiche\n\n#### 5.8 **Qualité de l'air** (`louka/air.py`)\n- **Source** : RSQAQ (Réseau de surveillance de la qualité de l'air du Québec) + PST (Procédure de surveillance transfrontalière)\n- **Stockage** : `data/air.db` (cache de stations)\n- **Lookup** : station RSQAQ/PST la plus proche (/<1500 m typiquement)\n- **Affichage** : bloc \"Qualité de l'air\" fiche (indice, polluants critiques)\n\n#### 5.9 **Stations-service** (`louka/gaz.py`)\n- **Source** : gazquebec.ca (scraping prix courants)\n- **Stockage** : `data/gaz.db`\n- **Affichage** : bloc \"Stations-service\" fiche (prix essence actuel, distance)\n\n#### 5.10 **Estimation coût électricité Hydro-Québec** (`louka/hydro.py`)\n- **Source** : Session publique Hydro-Québec (SPA rétro-conçu)\n- **API** : POST lieu-consommation + POST consommation/estimation\n- **Authentification** : reCAPTCHA v2 + proxy résidentiel (Oxylabs → Bright Data)\n- **Configuration** : `HQ_CAPTCHA_KEY` + `HQ_CAPTCHA_PROVIDER` (2captcha ou CapSolver)\n- **Stockage** : `data/hydro.db` (cache par adresse)\n- **Affichage** : bloc \"Estimation coût électricité\" fiche ($/année, classe de consommation)\n\n#### 5.11 **Commerces et transport** (`louka/commerces.py`)\n- **Source** : Mapbox Geocoding + OpenStreetMap\n- **Affichage** : bloc \"Commerces et transport\" fiche (épiceries, pharmacies, parcs, écoles, soins, transports)\n- **API** : endpoint `/api/commerces?lat=…&lng=…`\n\n#### 5.12 **KA Scores** (`louka/kascores.py`)\nVoir section 2 ci-dessus. Résumé :\n- **Walk** : distance moyennes besoins quotidiens (épicerie 400 m, pharmacie 400 m, parc 300 m…)\n- **Transit** : arrêt bus ≤600 m, métro ≤1500 m, PMD StatCan 2021 percentile\n- **Bike** : km cyclables 1 km rayon\n- **Calme** : pénalités distance routes/rails/aéro/industriel, bonus parcs\n- **Services** : familles commerces (1 km), santé (3 km), éducation (3 km), loisirs (1 km) par saturation\n- **Global** : moyenne pondérée (défaut {walk: 0.30, transit: 0.20, bike: 0.15, calme: 0.20, services: 0.15})\n- **Détail** : JSON expliquant calcul par score\n- **Version** : \"2026.08-v1\" (recalcul parc si changement)\n\n#### 5.13 **Contrôle qualité des images** (`louka/imgcheck.py`)\n- **Vérification par URL** : mort (404/410), dimension (<250 × 180), fichier (<6 Ko), hash sha1 (doublons/placeholders)\n- **Placeholder détection** : image générique présente sur ≥12 annonces + ≥30 % source + ≥8 adresses différentes\n- **Cache** : `image_checks` table (recheck 7 jours après échec)\n- **Résultat** : colonne `images_ok` (galerie nettoyée, URLs mortes retirées)\n- **Audit** : colonne `img_audit` (JSON traçabilité)\n\n#### 5.14 **Description structurée (Text Mining)** (`louka/textmine.py`)\n- **Fonction** : `analyser(description, price, sector, city)`\n- **Output** : digest (JSON)\n - `texte_nettoye` (nettoyage regex)\n - `en_bref` (résumé court)\n - `sections` (découpage par tirets/numéros)\n - `faits` (extraction structurée : prix, dispo, durée bail, électroménagers, inclusions, contraintes, dépôt, quartier)\n - `confiance` (scores de confiance par champ)\n - `incoherences` (prix, dates)\n - `completude` (%)\n- **Regex pure** : aucun LLM (note README ligne 105)\n- **Langue** : français québécois\n\n---\n\n### 6. JOBS/WORKERS/CRONS\n\n#### PM2 processes (production, README lignes 160-168)\n| Process | Commande | Rôle | Cadence |\n|---------|----------|------|---------|\n| `lou-ka-web` | `.venv/bin/python run.py serve 8095` | API + frontend | continu |\n| `lou-ka-sync` | `.venv/bin/python run.py watch 60` | Resync sources | 60 min |\n| `lou-ka-ngrok` | `ngrok http 8095` | Tunnel HTTP vers www.lou-ka.com | continu |\n\n#### Tâches de fond (boucle `ingest.watch()`, lignes 91-152 de run.py)\n**Ordre d'exécution à chaque cycle (60 min par défaut)** :\n1. `ingest.run()` — sync toutes sources + circuit breaker\n2. `geocode.run_batch()` — géocodage par lots Adresses Québec (nouveau)\n3. `poi.run(limit=80)` — POI des nouveaux immeubles\n4. `quartier.enrich()` — DAUID des nouvelles annonces\n5. `quality.backfill()` — recalcul completeness/published (après geocode)\n6. `imgcheck.run(1500)` — audit images par lots incrémentaux\n7. `dedup.run()` — déduplication inter-sources (APRÈS geocode : coordonnées requises)\n8. `fairvalue.compute_all()` — juste valeur (APRÈS dedup/geocode/quality)\n9. `environment.run(limit=4)` + `kascores.run()` — KA Scores nouveaux immeubles\n10. `hydro.precompute(limit=100, budget=30)` — estimation électricité (coût borné)\n11. `tal.precompute(limit=60, budget=600)` — TAL file prioritaire + backfill (coût borné)\n\n#### Commandes CLI (run.py main, lignes 35-134)\n| Commande | Rôle |\n|----------|------|\n| `python run.py sync [sources]` | Ingestion unique (toutes ou ciblées) |\n| `python run.py watch [minutes]` | Boucle (60 min défaut) |\n| `python run.py sync-ct [sources]` | Ingestion court terme |\n| `python run.py watch-ct [minutes]` | Boucle court terme (360 min défaut) |\n| `python run.py serve [port]` | API + frontend (8080 défaut, 8095 prod) |\n| `python run.py geocode [n]` | Géocodage (ancien mode 1-par-1) |\n| `python run.py quality [n]` | Recalcul completeness (touts ou n) |\n| `python run.py imgaudit [n] [source]` | Audit images (1500 défaut, optionnel par source) |\n| `python run.py fairvalue` | Recalcul juste valeur (tout parc) |\n| `python run.py poi [n]` | POI (n requêtes max) |\n| `python run.py quartier [n]` | Quartier/DAUID (n annonces) |\n| `python run.py env [n]` | Environnement OSM tuiles (n tuiles) |\n| `python run.py kascores [all]` | KA Scores (incr. ou all=tout parc) |\n| `python run.py inondation-build` | Reconstruction index inondation |\n| `python run.py air-refresh` | Refresh données RSQAQ |\n| `python run.py gaz-refresh` | Refresh stations-service |\n| `python run.py hydro-precompute [n] [b]` | Estimation Hydro (n annonces, budget b sec) |\n| `python run.py rdl` | Refresh Registre des loyers |\n| `python run.py tal [n] [b]` | TAL SOQUIJ (n adresses, budget b sec) |\n| `python run.py record [sources]` | Enregistrement fixtures HTTP test |\n\n#### Caches / circuit breaker\n- **Connecteur breaker** : 5 échecs consécutifs → skip, retry tous les 5 passages\n- **SQLite WAL** : lectures (web) + écritures (sync, imgcheck) concurrentes sans verrou global ; `busy_timeout=30000`\n- **Géocodage** : 1 req/sec politesse\n- **POI** : 1 req/sec politesse, cache 3 mois\n- **TAL** : 1 req/sec Scrapfly, 6 mois refresh, budget secondes limité\n- **Hydro** : TTL succès 180 j, TTL échecs 30 j\n- **Tiles OSM** : cache 3 mois\n\n---\n\n### 7. CHAMPS COLLECTÉS MAIS NON AFFICHÉS\n\n**Colonnes dans `listings` collectées mais rarement utilisées côté frontend** :\n- `details.* json structure` : parking, appliances, contact (téléphone/email) — affichés dans chips/inclusions, pas exploités en recherche avancée\n- `digest.incoherences` : incohérences détectées (prix contradictoires, dates absurdes) — loggé mais pas affiché\n- `digest.confiance` : scores de confiance extraction (haute/faible par champ) — calculé, pas utilisé\n- `img_audit` : détail contrôle images (URLs retirées, raisons) — traçabilité, pas affiché\n\n**Champs présents mais nulls/peu peuplés** :\n- `pets`, `furnished` : null pour ~40-50 % du parc (absent source)\n- `availability` vs `availability_date` : texte source rarement exact\n- `bedrooms`, `bathrooms` : null si pas extractible (extraction robuste mais imparfaite)\n- `details.contact` : téléphone/email rarissime (données source non structurées)\n\n---\n\n### 8. GÉOSPATIAL (Lat/Lng, Géocodage, Routing)\n\n#### Stockage et indexation\n- **Colonnes** : `listings.lat`, `listings.lng` (REAL, nullable)\n- **Index** : `idx_listings_geo` sur (lat, lng) — utilisé par requêtes bbox (carte 3D)\n- **Clé immeuble** : `coord_key = \"{round(lat, 4)},{round(lng, 4)}\"` (~11 m ≈ unités même immeuble)\n - Jointure `listings.coord_key = kascores.coord_key` (partage KA Scores immeuble)\n - Jointure `listings.coord_key = poi_cache.coord_key` (partage POI)\n\n#### Géocodage\n- **Primaire** : Nominatim (OSM) — gratuit, 1 req/sec\n- **Secondaire** : Adresses Québec (MERN) — rues récentes\n- **Cache** : `geocode_cache` (provider, lat, lng, failed flag)\n- **Normalisation clé** : adresse sans numéro app (regex ligne 56 geocode.py)\n- **Validation** : coords source rejetées si hors bounding box province\n\n#### Distance (Haversine, pas routing)\n- **Fonction** : `_haversine_m(lat1, lng1, lat2, lng2)` (poi.py ligne 63-68, kascores.py, tal.py)\n- **Formula** : terre sphérique R=6371 km, calcul en mètres\n- **Pas de routage** : vol d'oiseau × 1.3 (facteur réseau usuel — kascores.py ligne 39 DETOUR)\n- **Pas de PostGIS** : calculs Python pure ou SQL simple (`BETWEEN lat±delta, lng±delta`)\n\n#### Index spatial (grille)\n- **KA Scores** : grille de recherche voisinage (kascores.py `Grid` classe, ligne 85-107)\n - Cellule ~0,01° ≈ 1,1 km\n - Recherche dans rayon par balayage cellules voisines\n- **POI** : tuile 0.5° (poi.py ligne 36) → download par tuile, calcul local\n\n#### Pas d'isochrone / réseau de routage\n- Aucune API OSRM/Vroom/Mapbox Directions intégrée\n- Distance à vol d'oiseau uniquement\n\n#### Affichage carte 3D\n- **Framework** : `@groupe-ka/ka-maps` (wrapper Mapbox GL JS)\n- **Clusters** : par prix moyen (front-end)\n- **Sync liste ↔ carte** : une même requête `/api/search` retourne `listings` (page 1-100) + `points` (tous filtrés, format compact)\n\n---\n\n### 9. PHOTOS / IMAGES\n\n#### Stockage\n- **URLs sources** : collectées dans `listings.images` (JSON liste URLs)\n- **Téléchargement** : non — on conserve URLs sources (référence au stockage CDN du gestionnaire)\n- **Proxy** : endpoint `/api/img?u=&w=` rend miniatures WebP (lazy-loading frontend)\n - Widths : 160, 480, 800, 1280 px\n - Compression WebP q=80\n - Cache sur disque `/data/imgcache/_.webp`\n\n#### Qualité\n- **Audit** (`imgcheck.py`) : mort (404/410), minuscule (<250 × 180), petit (<6 Ko), placeholder (hash sha1 répété)\n- **Résultat** : colonne `images_ok` (galerie nettoyée, URLs mortes retirées)\n- **SHA1** : hash contenu image → détection doublons exact + detection placeholders (même image sur N annonces + N adresses)\n- **Cache URL** : table `image_checks` (status ok/dead/small, dimensions, bytes, sha1)\n\n#### Affichage\n- Frontend : galerie native (lightbox balayage + pincement zoom)\n- Fallback : visuel placeholder Lou-Ka si toutes images mortes\n\n---\n\n### 10. ADRESSES\n\n#### Normalisation (`louka/normalize.py` + `louka/schema.py`)\n- **`clean_address()`** : suppression accents, espaces multiples, HTML entities\n- **`clean_city()`** : normalisation communes (Québec vs Quebec, Lévis vs Levis)\n- **`clean_sector()`** : quartiers/arrondissements\n\n#### Clé adresse (déduplication, `dedup.py` ligne 69-79)\n- **`_parse_address(address, city)`** : extrait (clé_adresse, unit_num)\n- Clé = \"civique|rue_significative|ville\" sans accents ni types de voie\n- Rejet types voie : rue, avenue, boulevard, chemin, place, etc. (FR + EN)\n- Unification points cardinaux : Ouest/West/O → \"o\", Est/East/E → \"e\"\n- Unité = numéro surnuméraire ou \"app/apt/suite/bureau\" + numéro\n\n#### Croisement TAL (par adresse normalisée)\n- Adresse recherche : \"no civique rue\" (mots clés, pas phrases guillemets supportées)\n- **TAL lookup** : jointure `tal_lookup.addr_key = clean_address(listings.address)`\n- Vérification : texte intégral décision confirme adresse exacte\n\n---\n\n## RÉSUMÉ VOLUMÉTRIQUE AU 2026-08-24\n\n| Métrique | Valeur | Notes |\n|----------|--------|-------|\n| **Listings actives** | 60 999 | `WHERE active=1` |\n| **Listings publiées** | 59 819 | Passent qualité, `WHERE published=1` |\n| **Listings en quarantaine** | 1 180 | Sous seuil complétude ou hors sujet (stationnement, garage, etc.) |\n| **Sources connecteurs** | 281 | Recensées dans `data/sources.json` |\n| **Connecteurs actifs** | 250 | 221 long terme + 29 court terme |\n| **Historique prix** | 95 767 entrées | 91 302 UIDs distincts (price_log) |\n| **Fair Value estimées** | 45 709 | ~75 % parc couvert |\n| **KA Scores calculés** | 28 438 immeubles | 95,5 % couverture parc |\n| **Géolocalisées** | ~59 000 | 97,4 % (coords lat/lng présentes) |\n| **Loyer médian** | 1 700 $ | Tous types |\n| **Villes couvertes** | 1 233 | Province entière |\n| **Régions** | 11 | (Montréal, Québec, Outaouais, Estrie, Mauricie, Gaspésie, etc.) |\n| **Séjours court terme** | 41 460 | Section `/court-terme` (base louka_ct.db) |\n| **BD image check** | ~100 000 URLs vérifiées | 4-5 % nouvelles par cycle |\n| **Registre TAL** | 57 433 déclarations | Registre-des-loyers.ca géocodées |\n| **TAL décisions** | ~15 000 | Historique tribunal du logement |\n| **BD Inondation** | Entière province | 493 Mo raster |\n| **BD Quartier** | 10 351 DA (aires diffusion) | Polygones 2021, stats, demos, crimes |\n\n---\n\n## DONNÉES NON PÉRENNISÉES (Colonnes Futures Utiles)\n\nPour votre upgrade, **champs existants à exploiter** :\n- `dup_sources` (JSON) : autres sources même annonce → enrichissement \"aussi publiée sur X gestionnaires\"\n- `dauid` : jointure quartier.db existante → accélération peuplage stats quartier\n- `digest` (JSON) : structuration description existante → réutiliser plutôt que refondre\n- `details` (JSON) : inclusions/parking/appliances → exploiter pour filtres avancés client\n\n**Champs à ajouter pour fiches enrichies** :\n- `building_id` (TEXT, fk) : lien vers entité Immeuble future\n- `landlord_id` (TEXT, fk) : lien vers entité Gestionnaire future\n- `price_per_sqft` (REAL) : $/pi² calculé si area_sqft présente (denormalisation utile)\n- `price_history_json` (TEXT JSON) : cache prix 12 mois (denormal. perf)\n- `google_reviews_avg` (REAL), `google_reviews_count` (INT) : avis Google gestionnaire (SerpApi)\n- `alert_recycled` (INT) : 1 si détecté recyclage (même titre, description, photos sur multiples adresses différentes)\n- `quality_score_explainable` (TEXT JSON) : raisons détaillées score complétude (actuellement raw list)\n\n---\n\n## ARCHITECTURE DÉCISIONNELLE\n\n**En place** :\n- ✓ Schéma central único (Listing dataclass → finalize() → normalisation commune)\n- ✓ Circuit breaker connecteurs\n- ✓ Déduplication inter-sources (autorité source)\n- ✓ Contrôle qualité à l'ingestion (completeness + pertinence)\n- ✓ Historique prix (price_log)\n- ✓ Enrichissement par étapes (geocode → poi → quartier → quality → dedup → fairvalue → kascores)\n- ✓ Cache spatial (poi_cache, geocode_cache, env_tiles)\n\n**À implémenter (votre upgrade)** :\n- Entité Immeuble (agrégation unités, metadata immeuble, gestionnaire locataire des lieux)\n- Entité Gestionnaire (profile détaillé, avis Google via SerpApi, coordonnées)\n- Snapshot annonce (archive complète par dates clés = historique détail fiche)\n- Détection annonces recyclées (comparaison texte/images cross-addresses)\n- Coût réel mensuel dénormalisé (prix + hydroest + chauffage-inclus)\n- Prix/pi² calculé (colonne supplémentaire utile pour tri/filter)\n- KA Scores expliquables (détails déjà présents dans JSON `kascores.details`, à afficher)\n\nBonne chance pour l'upgrade! Le repo est très bien structuré — respect sur l'architecture!"}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e51385cf-1536-4004-a352-1e7d19fea443","timestamp":"2026-08-24T21:48:15.708Z","tool_use_result":{"status":"completed","prompt":"Fais un audit TRÈS APPROFONDI (very thorough) du repo /Users/simon-pierreboucher/apps/lou-ka (agrégateur québécois de logements locatifs, en production). C'est de la recherche seulement — n'écris aucun code.\n\nJe prépare un upgrade majeur des fiches logement (historique d'annonces/snapshots, entité Immeuble, entité Gestionnaire + avis Google via SerpApi, coût réel mensuel, prix/pi², détection d'annonces recyclées, scores explicables). J'ai besoin de comprendre l'existant pour NE PAS reconstruire ce qui existe.\n\nRapporte précisément (avec chemins de fichiers + numéros de lignes clés) :\n\n1. **Stack** : frontend (framework, build), backend (langage, framework, serveur), base de données (SQLite/Postgres? ORM ou SQL brut?), comment l'app est servie (PM2? port?).\n2. **Modèle de données** : schéma complet des tables (listings, prix, photos, etc.), où le schéma est défini/migré. Y a-t-il déjà des tables building/immeuble, gestionnaire/landlord, historique de prix, snapshots?\n3. **Crawlers/importers** : liste des connecteurs/sources, comment ils tournent (cron? PM2?), pipeline de normalisation (adresse, prix, superficie), déduplication existante, fichier quality.py et relevance_reason().\n4. **Génération des fiches** : comment une fiche logement est rendue (SSR? API + JS? templates?), structure de la page détail, composants UI réutilisables (cartes, KPI, tableaux, pastilles — il y a eu une passe design « éditorial sharp »), le PDF (fiche PDF récente).\n5. **Enrichissements existants** : où et comment sont calculés/stockés : analyse juste valeur, comparables, distribution loyers, Registre des loyers, historique TAL, inondation, qualité de l'air, commerces, transport, essence, KA Scores, démographie, criminalité, îlots de chaleur. Quelles APIs/datasets externes sont utilisés (clés dans .env?).\n6. **Jobs/workers** : jobs de fond, crons, files d'attente, systèmes de cache.\n7. **Champs disponibles mais non exploités** : colonnes/données collectées mais pas affichées.\n8. **Géospatial** : y a-t-il géocodage, lat/lon, PostGIS ou calculs haversine, moteur de routage/isochrones?\n9. **Photos** : stockage des photos (URLs? téléchargées?), hash d'images existant?\n10. **Adresses** : normalisation d'adresses existante (le module TAL croise déjà par adresse — comment?).\n\nStructure ton rapport par sections numérotées ci-dessus. Sois exhaustif mais factuel. Mentionne la taille de la base de données et le volume de données (nb listings, historique conservé ou non).","agentId":"a57e70d144471fbcc","agentType":"Explore","content":[{"type":"text","text":"Excellent. Maintenant j'ai une excellente compréhension. Générons un rapport complet et détaillé.\n\n---\n\n## RAPPORT AUDIT APPROFONDI — LOU-KA (Agrégateur Québécois de Logements)\n\n### 1. STACK TECHNOLOGIQUE\n\n#### Backend\n- **Langage/Framework** : Python 3.14 + **FastAPI** (ligne 24 de `web.py`)\n- **Serveur HTTP** : **Uvicorn** (démarrage via `uvicorn.run()` ligne 131 de `run.py`)\n- **Port** : **8095** (mentionné README ligne 119, configuration PM2)\n- **Exécution** : Mode production via **PM2** (processus `lou-ka-web` exécutant `python run.py serve 8095`), plus synchronisation périodique (`lou-ka-sync` avec `python run.py watch 60` = 60 min, ligne 42 de `run.py`)\n\n#### Base de données\n- **Système** : **SQLite** avec **WAL** (Write-Ahead Logging — ligne 241-242 de `db.py`, `PRAGMA journal_mode=WAL`)\n- **Chemin** : `/Users/simon-pierreboucher/apps/lou-ka/data/louka.db` (défini ligne 18 de `db.py`, soit `DB_PATH`)\n- **ORM** : **SQL brut** (construction manuelle des requêtes SQL via `.execute()`, pas d'ORM comme SQLAlchemy)\n- **Migrations** : **Automatiques à la connexion** (fonction `connect()` ligne 221-243 de `db.py` exécute `CREATE TABLE IF NOT EXISTS` + `ALTER TABLE` pour colonnes manquantes)\n- **Taille actuelle** : **~1,5 Go** (`louka.db-wal` = 28 Mo WAL, `louka.db` = 1,47 Go, état au 2026-08-24)\n\n#### Frontend\n- **Framework** : **React 18.3.1** + **TypeScript 5.5.4**\n- **Build/Dev** : **Vite 5.4.0** (fichier `vite.config.ts`, scripts `npm run dev`, `npm run build`)\n- **Routage** : **React Router v6.26** (`react-router-dom`)\n- **Cartographie** : **Mapbox GL 3.28.1** + wrapper interne **`@groupe-ka/ka-maps`** (dépendance pointant vers `../../ka-maps`)\n- **Build output** : `/frontend/dist/` (SSR intégré côté backend via `louka/seo.py`)\n\n#### Persistance des enrichissements (bases annexes)\nToutes dans `/data/` :\n- `quartier.db` (120 Mo) : aires de diffusion 2021, stats de quartier, démographie, crimes\n- `tal.db` (20 Ko) : historique Tribunal administratif du logement (SOQUIJ)\n- `rdl.db` (15 Mo) : Registre des loyers (déclarations publiques)\n- `hydro.db` (32 Ko) : cache des estimations électricité Hydro-Québec\n- `air.db` (65 Ko) : qualité de l'air (RSQAQ/PST)\n- `gaz.db` (454 Ko) : stations-service (gazquebec.ca)\n- `inondation.db` (493 Mo) : zones inondables (BDZI gouvernement du Québec)\n- `commerces.db` (106 Ko) : cache POI commerces\n- `transit.db` (3,7 Mo) : arrêts et lignes transport en commun\n\n---\n\n### 2. MODÈLE DE DONNÉES (Schéma Complet)\n\n#### Table `listings` (annonces locatives)\n**Colonnes clés** (définies en détail aux lignes 31-177 de `db.py`, _SCHEMA + _MIGRATIONS) :\n\n| Colonne | Type | Rôle | Etat |\n|---------|------|------|------|\n| `uid` | TEXT PK | `{source}:{external_id}` — clé unique | Primaire |\n| `source` | TEXT NOT NULL | ID connecteur (ex. \"msi\", \"logisco\") | Indexée |\n| `external_id` | TEXT NOT NULL | Identifiant chez la source | — |\n| `url` | TEXT | URL annonce originale | — |\n| `title`, `address`, `sector`, `city` | TEXT | Libellés d'affichage normalisés | — |\n| `unit_type` | TEXT | \"1½\", \"2½\", \"3½\"… \"Studio\", \"Chambre\" (schéma.py) | — |\n| `bedrooms`, `bathrooms` | REAL | Nombre chambre fermées/salles bain | Nullable |\n| `price` | REAL | Loyer mensuel $/mois | Nullable (si absent = quarantaine) |\n| `price_label` | TEXT | Texte original (\"à partir de 799$\") | — |\n| `availability`, `availability_date` | TEXT | Texte + ISO \"2026-07-01\" ou \"now\" | — |\n| `area_sqft` | REAL | Superficie pi² | Nullable |\n| `pets`, `furnished` | TEXT, INT | \"oui\"/\"non\"/\"conditions\", boolean | Nullable |\n| `description` | TEXT | Corps brut | — |\n| `digest` | TEXT (JSON) | Description structurée (textmine.py) | — |\n| `amenities` | TEXT (JSON) | Liste commodités sources | — |\n| `details` | TEXT (JSON) | Champs structurés : `{\"inclusions\": {\"heating\": true}, \"parking\": {...}}` | — |\n| `images` | TEXT (JSON) | URLs photos brutes | — |\n| `images_ok` | TEXT (JSON) | Galerie nettoyée (imgcheck.py) — priorité sur `images` | Nullable |\n| `img_audit` | TEXT (JSON) | Audit de qualité des images | Nullable |\n| `lat`, `lng` | REAL | Coordonnées géocodées | Nullable, indexées (`idx_listings_geo`) |\n| `coord_key` | TEXT | Clé immeuble \"lat,lng\" (4 déc ≈ 11 m) — jointure KA Scores | Indexée |\n| `geocode_failed` | INT | Flag si géocodage échoué définitif | Default 0 |\n| `dauid` | TEXT | Aire de diffusion 2021 StatCan — jointure quartier.db | Nullable |\n| `content_hash` | TEXT | SHA256 du contenu (détection changements) | — |\n| `first_seen`, `last_seen`, `updated_at` | REAL (timestamp unix) | Timestamps | — |\n| `active`, `miss_count` | INT | active=0 après délai de grâce (MISS_GRACE=2), miss_count incrémenté | Indexée (`idx_listings_active`) |\n| `published`, `completeness`, `quality_reasons` | INT, REAL, JSON | Score 0-100, seuil PUBLISH_THRESHOLD=40, raisons quarantaine | — |\n| `dup_of`, `dup_sources` | TEXT, JSON | Déduplication inter-sources (dedup.py) | Nullable |\n\n**Indexes** :\n- `uid` (PRIMARY KEY)\n- `idx_listings_source` (source)\n- `idx_listings_city` (city)\n- `idx_listings_active` (active, published)\n- `idx_listings_geo` (lat, lng)\n- `idx_listings_pub` (active, published)\n- `idx_listings_coord` (coord_key)\n\n**Taille** : **60 999 listings actives** (active=1), **59 819 publiées** (published=1 ET active=1), **1 180 en quarantaine** — au 2026-08-24\n\n#### Table `price_log` (historique des prix)\n```sql\nCREATE TABLE price_log (\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL, -- timestamp insertion\n price REAL -- NULL = retrait de l'affichage\n);\n```\n- **Taille** : 95 767 entrées = 91 302 UIDs distincts\n- **Rôle** : Suivi mensuel/historique (affiché sur fiche : \"prix_history\" — 6 derniers en ligne 656 de `web.py`)\n- **Insertion** : Automatique lors du sync (ligne 356-358, 382-384 de `db.py`)\n\n#### Table `fairvalue` (juste valeur locative)\n```sql\nCREATE TABLE fairvalue (\n uid TEXT PRIMARY KEY, -- listings.uid\n fv REAL, fv_low REAL, fv_high REAL, -- estimation + fourchette p25/p75\n deviation REAL, -- (prix - fv) / fv\n verdict TEXT, -- \"sous\" | \"marche\" | \"sur\" | NULL\n confidence TEXT, -- \"fort\" | \"moyen\" | \"faible\"\n method TEXT, -- N1 | N2 | N3 (voir fairvalue.py)\n comps INTEGER, -- nombre de comparables (kNN)\n segment TEXT, -- clé segment {ville}/({quartier}) × bedrooms\n model_version TEXT, -- \"fv-logement-1.0\" (ligne 29 de fairvalue.py)\n computed_at REAL\n);\n```\n- **Taille** : 45 709 entrées (74 % du parc)\n- **Rôle** : Badge marché \"Bon deal\" / \"Dans le marché\" / \"Au-dessus\" sur les listings\n- **Algorithme** (fairvalue.py) :\n - **N1** : Segment de référence (ville × quartier × bedrooms) = médiane + p25/p75\n - **N2** : Ajustements multiplicatifs par features (meublé, chauffage, parking)\n - **N3** : Comparables kNN géographiques (k=12, rayon 3 km, pondération gaussienne)\n - Combinaison + couloir de sécurité (70 %-130 % fourchette p25/p75)\n\n#### Table `kascores` (scores maison 0-100 par immeuble)\n```sql\nCREATE TABLE kascores (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi 4 déc\n lat REAL, lng REAL,\n walk REAL, -- marchabilité besoins quotidiens\n transit REAL, -- desserte transport collectif\n bike REAL, -- praticité vélo\n calme REAL, -- tranquillité estimée\n services REAL, -- richesse services (1 km / 3 km)\n global REAL, -- moyenne pondérée (customisable côté client)\n details TEXT (JSON), -- détail par score\n version TEXT, -- \"2026.08-v1\" (ligne 38 de kascores.py)\n computed_at REAL\n);\n```\n- **Taille** : 28 438 immeubles (coord_key distincts)\n- **Couverture** : 95,5 % du parc (cf. README ligne 106)\n- **Sources de données** :\n - OpenStreetMap via Overpass (POI, routes, rails, cyclable, bars)\n - StatCan PMD 2021 (fréquence/qualité transport collectif)\n - Aucune donnée inventée : secteur sans données → NULL\n- **Barème** (kascores.py, lignes 43-66) :\n - Walk : épicerie/pharmacie/parc/café/école/clinique/garderie/dépanneur/gym/bibliothèque\n - Transit : arrêt bus (≤600m), métro (≤1500m), PMD percentile\n - Bike : km cyclables dans 1 km\n - Calme : distance routes/rails/aéro/industriel (pénalités), bonus parcs\n - Services : familles (commerces, santé, education, loisirs) par rayon + saturation\n\n#### Tables de cache spatial\n```sql\nCREATE TABLE poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" 4 déc\n pois TEXT (JSON), -- [{\"cat\": \"epicerie\", \"name\": \"Maxi\", \"dist_m\": 450}, ...]\n ts REAL\n);\n\nCREATE TABLE geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (normalize.py)\n lat REAL, lng REAL,\n provider TEXT, -- \"nominatim\" | \"adresses-quebec\"\n failed INT DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE env_tiles (\n tile_key TEXT PRIMARY KEY, -- \"ty,tx\" tuiles 0.5°\n data TEXT (JSON), -- routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n```\n\n#### Tables de contrôle qualité\n```sql\nCREATE TABLE image_checks (\n url TEXT PRIMARY KEY,\n status TEXT, -- \"ok\" | \"dead\" | \"small\" | \"error\"\n width INTEGER, height INTEGER, bytes INTEGER,\n sha1 TEXT, -- hash détection doublons/placeholders\n checked_at REAL\n);\n\nCREATE TABLE detail_cache (\n source TEXT, external_id TEXT,\n key TEXT, -- hash liste source\n payload TEXT (JSON), -- payload détail si clé inchangée\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n```\n\n#### Tables utilisateurs et favoris\n```sql\nCREATE TABLE users (\n id INT PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID \"sub\")\n ka_id TEXT, -- Groupe KA \"ka-0123456789\"\n email TEXT,\n name TEXT, picture TEXT, -- avatar Google\n public INT DEFAULT 0, -- profil public /u/{ka_id}\n role TEXT, -- \"locataire\" | \"gestionnaire\" | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n display_name TEXT, avatar_url TEXT, -- profil custom\n bio TEXT, city TEXT, phone TEXT, website TEXT,\n socials TEXT (JSON), -- {instagram, facebook, x, linkedin…}\n created_at REAL, last_login REAL\n);\n\nCREATE TABLE favorites (\n user_id INT NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n```\n\n#### Tables administratives\n```sql\nCREATE TABLE sync_log (\n id INT PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INT, added INT, updated INT, removed INT,\n ok INT, -- 1 = succès, 0 = erreur\n message TEXT, -- \"ok\" | alerte dérive | message erreur\n stats TEXT (JSON) -- {\"null_price_rate\": 0.05, \"alert\": \"…\"}\n);\n\nCREATE TABLE source_profiles (\n source_id TEXT PRIMARY KEY, -- data/sources.json[].id\n owner_user_id INT,\n tagline TEXT, description TEXT,\n website TEXT, phone TEXT, email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE fv_segments ( -- (pré-calculée, pas insérée directement)\n segment TEXT PRIMARY KEY, -- \"quebec/beauport|2\" = clé N1\n median REAL, p25 REAL, p75 REAL,\n count INT, -- volume du segment\n updated_at REAL\n);\n```\n\n#### Tables court terme (louka/shortterm/)\nSchéma similaire dans `data/louka_ct.db` (238 Mo) — voir `louka/shortterm/schema.py` + `louka/shortterm/db.py`\n\n---\n\n### 3. CRAWLERS/CONNECTEURS ET PIPELINE D'INGESTION\n\n#### Registre des connecteurs\n- **Découverte auto** : `louka/connectors/__init__.py` (lignes 9-32) — tout module contenant une classe héritant de `BaseConnector` avec `source_id` non vide est enregistré automatiquement\n- **Base de classe** : `louka/connectors/base.py` (interface standardisée)\n- **Nombre total** : **250 connecteurs** (221 long terme + 29 court terme)\n - 281 sources recensées dans `data/sources.json` (certaines hors-ligne ou fusionnées)\n\n#### Connecteurs long terme (louka/connectors/)\nExemples d'architectures variées :\n- **Directes (HTML/API)** : `msi.py`, `logisco.py`, `laberge.py`, `cogir.py` — sites gérés directement\n- **Via Firecrawl** : extraction JavaScript/SPA (site clé : `FIRECRAWL_API_KEY` dans `.env`)\n- **Via Scrapfly** : rendu JS + anti-bot (site clé : `SCRAPFLY_KEY`, utilisé aussi pour TAL)\n- **Via Bright Data Web Unlocker** : `apartments_com.py` (Akamai/challenges, site clé `BRIGHTDATA_API_KEY`)\n- **Via Oxylabs + proxy résidentiel** : chaîne résiliente Oxylabs → Scrapfly → Bright Data (see `hydro.py` ligne 54-64)\n- **Acteurs Apify maison** : `ka-fb-marketplace` (Facebook Marketplace) + `ka-apartments-com` (Apartments.com via Apify + Oxylabs)\n\nExemples de connecteurs :\n- `lafrance_mathieu.py`, `msi.py`, `logisco.py`, `laberge.py`, `cogir.py` — gestionnaires Québec/Lévis\n- `kijiji.py`, `lespac.py`, `fb_marketplace.py` — petites annonces / classifiés\n- `logisquebec.py`, `louer_ca.py`, `rentals_ca.py` — portails agrégateurs\n- `zumper.py`, `apartments_com.py` — portails étrangers (contenu pancanadien)\n- `rémax_quebec.py`, `royal_lepage.py`, `sutton.py` — courtiers immobiliers\n\n#### Pipeline d'ingestion (`louka/ingest.py`)\n**Fonction `run(sources: list | None)`** (ligne 39) :\n1. Boucle sur chaque connecteur (auto-découverts sauf si liste ciblée)\n2. **Circuit breaker** (lignes 16-23, 50-57) : source qui a échoué aux BREAKER_FAILS=5 passages consécutifs est sautée, retentée tous les BREAKER_RETRY_EVERY=5 passages (lissage des timeouts)\n3. Appel `cls().fetch()` pour chaque connecteur → retourne liste `Listing` (schéma.py)\n4. Finalisation `lst.finalize()` (ligne 66) : normalisation commune\n5. Synchronisation `db.sync_source(con, source, listings)` (ligne 69)\n\n**Synchronisation (`db.sync_source()`, lignes 279-427 de db.py)** :\n- **Nouveau** : insertion complète + entrée initiale dans `price_log`\n- **Modifié** (content_hash changé) : UPDATE avec new hash, images_ok/img_audit invalidées, entrée `price_log` si prix changé\n- **Inchangé** (hash identique) : UPDATE `last_seen`, reset `miss_count`\n- **Absent** (source n'a pas retourné) : incrémente `miss_count`, au passage MISS_GRACE=2 : `active=0`\n- **Dérive détectée** (lignes 250-276) : si volume ≤ 25 % de médiane 5 derniers passages, retraits suspendus → alerte dans `sync_log`\n\n**Qualité lors du sync** (lignes 306-337) : score de complétude + publication/quarantaine automatique (quality.py)\n\n#### Normalisation et déduplication\n\n**Normalisation (`louka/normalize.py`)**\n- `clean_address()`, `clean_city()`, `clean_sector()` — suppression accents, normes\n- `parse_price(price_label)` — extraction numérique\n- `parse_availability_date(availability)` — normalisé en ISO ou \"now\"\n- `parse_area_sqft(texte)` — recherche mention superficie\n- `parse_bedrooms()`, `parse_bathrooms()` — extraction texte ou type d'unité\n- `extract_details()`, `merge_details()` — champs structurés (inclusions, parking, appliances)\n\n**Finalisation (`Listing.finalize()`, lignes 84-159 de schema.py)**\n- Validation géographique : coordonnées source rejetées si hors province (44.5-63° N, -80° à -56° O)\n- Description structurée : appel `textmine.analyser()` si digeste absent\n- Remplissage chambre/salle bain par cascade : champ explicite → details structurés → type unité → extraction texte\n\n**Déduplication inter-sources (`louka/dedup.py`, récalcul complet à chaque passe)**\n1. **Regroupement** par proximité (grille lat/lng ≈110 m) OU clé adresse normalisée (civique + rue + ville sans types)\n2. **Confirmation** : même type unité (si connu) + prix ±4 % + signal géographique/adresse\n3. **Résolution** : source **la plus autoritaire** devient canonique (PORTALS=10, CLASSIFIEDS=20, gestionnaires=0)\n4. **Marquage** : `dup_of = uid_canonique`, `dup_sources = [source2, source3, …]` (badge \"aussi publiée sur…\")\n5. **Affichage** : web.py filtre `WHERE dup_of IS NULL`\n\n#### Quality control (`louka/quality.py`)\n**Fonction `evaluate(d: dict)`** (ligne 152) retourne (score, publiable, raisons) :\n\n| Critère | Poids | Calcul |\n|---------|-------|--------|\n| Prix 175-20 000 $/mois | 20 | 0 si absent, 20 sinon |\n| Adresse OU coords | 14 | 14 si adresse/coords, 7 si secteur seulement |\n| Ville | 4 | — |\n| Description ≥200 car | 14 | 14 si ≥200, 7 si ≥60 |\n| Images | 16 | 0→0, 1→6, 2-4→11, ≥5→16 |\n| Type d'unité ou bedrooms | 8 | — |\n| Disponibilité | 5 | — |\n| Superficie | 4 | — |\n| Salles de bain | 3 | — |\n| Commodités ≥3 | 5 | 5 si ≥3, 2,5 si ≥1 |\n| Details ≥3 champs | 4 | 4 si ≥3, 2 si ≥1 |\n| URL | 3 | — |\n\n**Portes dures** : annonce rejetée (published=0) si :\n- Pas de prix ou prix non plausible\n- Pas de localisation (adresse + coords + ville)\n- Pas de contenu exploitable (desc<40 car, pas commodités, pas type unité)\n- Pertinence manquante (voir fonction `relevance_reason()`, lignes 82-125)\n\n**Pertinence** (`relevance_reason()`) : quarantaine automatique pour :\n- Stationnement/garage (titre/description, prix <700$)\n- Local commercial/bureau\n- Location à la nuit (<600$, mention \"par nuit\")\n- Objets non résidentiels (prix <300$, aucun vocabulaire logement)\n\n**Seuil** : PUBLISH_THRESHOLD=40 — score <40 + aucune porte dure = publiée; score <40 + porte dure = quarantaine\n\n**Backfill** (`quality.backfill()`, ligne 240) : recalcul complet sur tous les actifs\n\n#### Historique de prix (`louka/db.py` + `web.py`)\n- Entrée `price_log` à chaque changement de prix ou retrait\n- Affichage fiche : 6 dernières (ligne 656 web.py)\n- Format : `[{ts: unix_timestamp, price: float | null}]`\n\n---\n\n### 4. GÉNÉRATION DES FICHES DE LOGEMENT\n\n#### Architecture de la fiche\n**Frontend** : `frontend/src/pages/Listing.tsx` (React, 800+ lignes) — SSR + client-side hydration\n**Backend** : `louka/web.py` endpoint `/api/listings/{uid}` (lignes 613-661) — données brutes\n**Backend API** : endpoints spécialisés pour composants (fairvalue, poi, quartier, TAL, RDL, air, hydro, gaz, commerces)\n\n#### Ordre des sections (standard Groupe KA, doc ligne 6-8 de Listing.tsx) :\n1. **Galerie** (lightbox fullscreen + balayage + pincement-zoom)\n2. **Prix + badges**\n - Loyer + \"à partir de\" si applicable\n - Badge marché (juste valeur) : \"Bon deal\" / \"Dans le marché\" / \"Au-dessus\"\n - Badge loyer vs secteur : `±X % vs le secteur`\n3. **Adresse + chips**\n - Adresse + coordonnées + secteur/ville\n - Badges commodités confirmées : \"Chauffage inclus\", \"Électricité incluse\", \"Laveuse-sécheuse\", \"Climatisation\", etc.\n4. **CTA + PDF** : lien vers annonce originale + bouton \"Télécharger fiche PDF\"\n5. **Description structurée** (digest)\n6. **Inclusions + détails pratiques** (parking, animaux, furnished, etc.)\n7. **Analyse de prix** (juste valeur : segment, comparables, confiance)\n8. **Emplacement** (mini-carte 3D Mapbox)\n9. **KA Scores** (Walk/Transit/Bike/Calme/Services + global avec justifications)\n10. **Quartier** (démographie, proximité, défavorisation, chaleur, criminalité)\n11. **À proximité** (commodités OSM groupées)\n12. **Enrichissements complémentaires**\n - Historique TAL (tribunal du logement)\n - Registre des loyers (déclarations communautaires)\n - Risque d'inondation (BDZI)\n - Qualité de l'air (RSQAQ)\n - Stations-service (gazquebec.ca)\n - Estimation coût électricité (Hydro-Québec)\n\n#### Composants réutilisables (frontend/src/components/)\n- `ListingCard.tsx` (640 px miniature, liste)\n- `SmartImg.tsx` (lazy-loading + fallback)\n- `FairValueBadge.tsx` (badge verdict + pourcentage)\n- `PriceAnalysis.tsx` (segment, comparables, distribution)\n- `KaScoresBlock.tsx` (5 pastilles 0-100 + détails)\n- `QuartierBlock.tsx` (démographie + proximité + criminalité + chaleur)\n- `HistoriqueTAL.tsx` (bloc décisions SOQUIJ)\n- `RegistreLoyers.tsx` (listes loyers déclarés autour)\n- `RisqueInondation.tsx` (zones BDZI)\n- `QualiteAir.tsx` (station RSQAQ + indice)\n- `EssenceProche.tsx` (prix courant stations gazquebec)\n- `CommercesProches.tsx` (Mapbox + POI groupés)\n- `HydroEstimation.tsx` (coût annuel estimé)\n- `ListingMap3D.tsx` (Mapbox GL lazy-loaded)\n- `MetroLignes.tsx` (lignes REM/métro Montréal)\n\n#### PDF (louka/pdfgen.py)\n**Fonction `fiche_pdf(uid)`** (généré à la demande `/api/listings/{uid}/pdf`) :\n- Généré avec **ReportLab** (lib Python PDF)\n- Palette Lou-Ka : blanc #ffffff, marine #0b1330, orange #ff6a00\n- Contenu : photos (jusqu'à 4 en grille), prix + analyse, chips, description structurée, inclusions, KA Scores, quartier, POI, QR code vers la fiche web\n- Taille page : Letter (8.5 × 11 in)\n\n**Rapport global** (`rapport_pdf()`) : agrégats du marché (totaux, régions, histogramme loyers, types, villes, gestionnaires)\n\n#### SSR (Server-Side Rendering)\n**Fichier** : `louka/seo.py` (1030 lignes)\n- Meta tags OpenGraph (og:image, og:title, og:description)\n- JSON-LD `RealEstateListing` + `Offer` (CAD)\n- Sitemaps dynamiques XML (chunks 10 000 URL)\n- Rendu HTML head côté serveur + body hydraté React client-side\n\n---\n\n### 5. ENRICHISSEMENTS EXISTANTS\n\n#### 5.1 **Géocodage d'adresses** (`louka/geocode.py`)\n- **Primaire** : Nominatim (OpenStreetMap, gratuit)\n- **Secondaire** : Adresses Québec (MERN — rues récentes, développements Lévis)\n- **Cache** : `geocode_cache` table (une adresse normalisée n'est géocodée qu'une fois par immeuble)\n- **Politesse** : 1 req/s (REQUEST_DELAY=1.1 sec)\n- **Validation** : coordonnées sources rejetées si hors province (bounding boxes par ville)\n- **Fonction** : `run_batch()` (ligne 59 de run.py) — traitement par lots Adresses Québec\n- **Erreurs** : flag `geocode_failed=1` si échouée → retry après 30 jours\n\n#### 5.2 **Commodités de proximité (POI)** (`louka/poi.py`)\n- **Source** : Overpass API (OpenStreetMap POI)\n- **Catégories** (54 de poi.py) : épicerie, dépanneur, pharmacie, école, garderie, parc, bus, métro, gym, café, clinique, hôpital, bibliothèque\n- **Stratégie tuiles** : téléchargement par tuile 0.5° (pas 1 req/immeuble coûteuse)\n- **Calcul** : haversine local, retenue du plus proche par catégorie\n- **Cache** : `poi_cache` par coord_key (4 déc = ±11 m, partage entre unités immeuble)\n- **Refresh** : tous les ~3 mois (REFRESH_AFTER=90 days)\n- **Affichage fiche** : groupes repliables (Courses, Transport, Études, Santé, Vie de quartier)\n\n#### 5.3 **Juste valeur locative (Fair Value)** (`louka/fairvalue.py`)\n- **Modèle** : `fv-logement-1.0` (versionnage = recalcul parc entier si changement)\n- **Trois niveaux** :\n - **N1** : Segment {ville/quartier} × bedrooms → médiane + p25/p75 (min 8 annonces)\n - **N2** : Ajustements multiplicatifs (meublé, chauffage, électricité, parking, climatisation, balcon, ascenseur, piscine, lave-vaisselle)\n - **N3** : Comparables kNN géographiques (k=12, rayon 3 km, pondération gaussienne exp(-(d/800m)²))\n- **Combination** : N1 + N3 weighted (poids kNN 0.6 si disponible)\n- **Fourchette de sécurité** : 70 % - 130 % fourchette p25/p75\n- **Verdict** : \"sous\" (<-8 %), \"marche\" (-8 % à +8 %), \"sur\" (>+8 %)\n- **Confiance** : \"fort\" / \"moyen\" / \"faible\" selon volume segment + kNN\n- **Recalcul** : automatique après dédup/geocode (ingest.py ligne 128-131)\n- **Stockage** : table `fairvalue` + table pré-calculée `fv_segments` (jointe en requête SQL)\n\n#### 5.4 **Analyse de quartier** (`louka/quartier.py`)\nSource unique : **`data/quartier.db`** (base statique construite par scripts — voir docs) :\n\n| Bloc | Données | Source |\n|------|---------|--------|\n| **Démographie** | population, densité, âge médian, revenu médian, % locataires, loyer moyen, % francophone, % universitaire | Recensement 2021 StatCan (DA_STATS) |\n| **Proximité** (PMD) | scores 0..1 de proximité à ressources (épiceries, pharmacies, écoles, parcs, transport) | StatCan Proximity Measures 2021 |\n| **Défavorisation** | quintiles matériel/social | INSPQ (Institut national santé publique) |\n| **Îlots de chaleur** | classe 1-9 (fraîcheur à chaleur) + écart temp | INSPQ (données liées immeuble via lat/lng) |\n| **Criminalité** | Montréal SPVM : points 12 mois + 12 précédents, par catégorie | SPVM (police Montréal) |\n| **Criminalité** | Autres villes : indice de gravité criminelle (IGC) | Services policiers locaux (Québec, Lévis, Laval, Longueuil) |\n\n**Jointure** : `da_poly` (point-dans-polygone par lat/lng) → DAUID (aire de diffusion 2021) → croisement tables stats\n\n**Stockage** : colonne `dauid` dans listings + références dans quartier.db\n\n#### 5.5 **Registre des loyers** (`louka/rdl.py`)\n- **API** : registre-des-loyers.ca (API JSON ouverte)\n- **Données** : 57 433 déclarations géocodées (vol d'oiseau, locataires volontaires)\n- **Affichage** : bloc \"Registre des loyers\" fiche + `/api/rdl?lat=…&lng=…` (rayon par défaut 600 m)\n- **Stockage** : table `rdl_housings` (base séparée `data/rdl.db`)\n- **Calcul** : médiane + écart-type par rayon autour de l'annonce\n\n#### 5.6 **Historique Tribunal administratif du logement (TAL)** (`louka/tal.py`)\n- **Source** : SOQUIJ (citoyens.soquij.qc.ca, tribunal 35)\n- **Stratégie** : Recherche « no civique rue » via Scrapfly (JS rendering + IP résidentielle), vérification adresse exacte dans texte intégral\n- **Cache** : table `tal_lookup` + `tal_decisions` dans `data/tal.db` (base séparée)\n- **Refresh** : révérification après ~6 mois (REFRESH_DAYS=180)\n- **Affichage** : bloc \"Historique TAL\" fiche (décisions par demandeur/type : non-paiement, résiliation, expulsion, reprise…)\n- **Stockage** : clé adresse normalisée (civique + rue + ville)\n\n#### 5.7 **Inondation** (`louka/inondation.py`)\n- **Source** : BDZI (Base de données des zones inondables, gouvernement du Québec)\n- **Stockage** : `data/inondation.db` (493 Mo — raster/tuiles)\n- **Lookup** : par lat/lng → classe de risque (récurence)\n- **Affichage** : bloc \"Risque d'inondation\" fiche\n\n#### 5.8 **Qualité de l'air** (`louka/air.py`)\n- **Source** : RSQAQ (Réseau de surveillance de la qualité de l'air du Québec) + PST (Procédure de surveillance transfrontalière)\n- **Stockage** : `data/air.db` (cache de stations)\n- **Lookup** : station RSQAQ/PST la plus proche (/<1500 m typiquement)\n- **Affichage** : bloc \"Qualité de l'air\" fiche (indice, polluants critiques)\n\n#### 5.9 **Stations-service** (`louka/gaz.py`)\n- **Source** : gazquebec.ca (scraping prix courants)\n- **Stockage** : `data/gaz.db`\n- **Affichage** : bloc \"Stations-service\" fiche (prix essence actuel, distance)\n\n#### 5.10 **Estimation coût électricité Hydro-Québec** (`louka/hydro.py`)\n- **Source** : Session publique Hydro-Québec (SPA rétro-conçu)\n- **API** : POST lieu-consommation + POST consommation/estimation\n- **Authentification** : reCAPTCHA v2 + proxy résidentiel (Oxylabs → Bright Data)\n- **Configuration** : `HQ_CAPTCHA_KEY` + `HQ_CAPTCHA_PROVIDER` (2captcha ou CapSolver)\n- **Stockage** : `data/hydro.db` (cache par adresse)\n- **Affichage** : bloc \"Estimation coût électricité\" fiche ($/année, classe de consommation)\n\n#### 5.11 **Commerces et transport** (`louka/commerces.py`)\n- **Source** : Mapbox Geocoding + OpenStreetMap\n- **Affichage** : bloc \"Commerces et transport\" fiche (épiceries, pharmacies, parcs, écoles, soins, transports)\n- **API** : endpoint `/api/commerces?lat=…&lng=…`\n\n#### 5.12 **KA Scores** (`louka/kascores.py`)\nVoir section 2 ci-dessus. Résumé :\n- **Walk** : distance moyennes besoins quotidiens (épicerie 400 m, pharmacie 400 m, parc 300 m…)\n- **Transit** : arrêt bus ≤600 m, métro ≤1500 m, PMD StatCan 2021 percentile\n- **Bike** : km cyclables 1 km rayon\n- **Calme** : pénalités distance routes/rails/aéro/industriel, bonus parcs\n- **Services** : familles commerces (1 km), santé (3 km), éducation (3 km), loisirs (1 km) par saturation\n- **Global** : moyenne pondérée (défaut {walk: 0.30, transit: 0.20, bike: 0.15, calme: 0.20, services: 0.15})\n- **Détail** : JSON expliquant calcul par score\n- **Version** : \"2026.08-v1\" (recalcul parc si changement)\n\n#### 5.13 **Contrôle qualité des images** (`louka/imgcheck.py`)\n- **Vérification par URL** : mort (404/410), dimension (<250 × 180), fichier (<6 Ko), hash sha1 (doublons/placeholders)\n- **Placeholder détection** : image générique présente sur ≥12 annonces + ≥30 % source + ≥8 adresses différentes\n- **Cache** : `image_checks` table (recheck 7 jours après échec)\n- **Résultat** : colonne `images_ok` (galerie nettoyée, URLs mortes retirées)\n- **Audit** : colonne `img_audit` (JSON traçabilité)\n\n#### 5.14 **Description structurée (Text Mining)** (`louka/textmine.py`)\n- **Fonction** : `analyser(description, price, sector, city)`\n- **Output** : digest (JSON)\n - `texte_nettoye` (nettoyage regex)\n - `en_bref` (résumé court)\n - `sections` (découpage par tirets/numéros)\n - `faits` (extraction structurée : prix, dispo, durée bail, électroménagers, inclusions, contraintes, dépôt, quartier)\n - `confiance` (scores de confiance par champ)\n - `incoherences` (prix, dates)\n - `completude` (%)\n- **Regex pure** : aucun LLM (note README ligne 105)\n- **Langue** : français québécois\n\n---\n\n### 6. JOBS/WORKERS/CRONS\n\n#### PM2 processes (production, README lignes 160-168)\n| Process | Commande | Rôle | Cadence |\n|---------|----------|------|---------|\n| `lou-ka-web` | `.venv/bin/python run.py serve 8095` | API + frontend | continu |\n| `lou-ka-sync` | `.venv/bin/python run.py watch 60` | Resync sources | 60 min |\n| `lou-ka-ngrok` | `ngrok http 8095` | Tunnel HTTP vers www.lou-ka.com | continu |\n\n#### Tâches de fond (boucle `ingest.watch()`, lignes 91-152 de run.py)\n**Ordre d'exécution à chaque cycle (60 min par défaut)** :\n1. `ingest.run()` — sync toutes sources + circuit breaker\n2. `geocode.run_batch()` — géocodage par lots Adresses Québec (nouveau)\n3. `poi.run(limit=80)` — POI des nouveaux immeubles\n4. `quartier.enrich()` — DAUID des nouvelles annonces\n5. `quality.backfill()` — recalcul completeness/published (après geocode)\n6. `imgcheck.run(1500)` — audit images par lots incrémentaux\n7. `dedup.run()` — déduplication inter-sources (APRÈS geocode : coordonnées requises)\n8. `fairvalue.compute_all()` — juste valeur (APRÈS dedup/geocode/quality)\n9. `environment.run(limit=4)` + `kascores.run()` — KA Scores nouveaux immeubles\n10. `hydro.precompute(limit=100, budget=30)` — estimation électricité (coût borné)\n11. `tal.precompute(limit=60, budget=600)` — TAL file prioritaire + backfill (coût borné)\n\n#### Commandes CLI (run.py main, lignes 35-134)\n| Commande | Rôle |\n|----------|------|\n| `python run.py sync [sources]` | Ingestion unique (toutes ou ciblées) |\n| `python run.py watch [minutes]` | Boucle (60 min défaut) |\n| `python run.py sync-ct [sources]` | Ingestion court terme |\n| `python run.py watch-ct [minutes]` | Boucle court terme (360 min défaut) |\n| `python run.py serve [port]` | API + frontend (8080 défaut, 8095 prod) |\n| `python run.py geocode [n]` | Géocodage (ancien mode 1-par-1) |\n| `python run.py quality [n]` | Recalcul completeness (touts ou n) |\n| `python run.py imgaudit [n] [source]` | Audit images (1500 défaut, optionnel par source) |\n| `python run.py fairvalue` | Recalcul juste valeur (tout parc) |\n| `python run.py poi [n]` | POI (n requêtes max) |\n| `python run.py quartier [n]` | Quartier/DAUID (n annonces) |\n| `python run.py env [n]` | Environnement OSM tuiles (n tuiles) |\n| `python run.py kascores [all]` | KA Scores (incr. ou all=tout parc) |\n| `python run.py inondation-build` | Reconstruction index inondation |\n| `python run.py air-refresh` | Refresh données RSQAQ |\n| `python run.py gaz-refresh` | Refresh stations-service |\n| `python run.py hydro-precompute [n] [b]` | Estimation Hydro (n annonces, budget b sec) |\n| `python run.py rdl` | Refresh Registre des loyers |\n| `python run.py tal [n] [b]` | TAL SOQUIJ (n adresses, budget b sec) |\n| `python run.py record [sources]` | Enregistrement fixtures HTTP test |\n\n#### Caches / circuit breaker\n- **Connecteur breaker** : 5 échecs consécutifs → skip, retry tous les 5 passages\n- **SQLite WAL** : lectures (web) + écritures (sync, imgcheck) concurrentes sans verrou global ; `busy_timeout=30000`\n- **Géocodage** : 1 req/sec politesse\n- **POI** : 1 req/sec politesse, cache 3 mois\n- **TAL** : 1 req/sec Scrapfly, 6 mois refresh, budget secondes limité\n- **Hydro** : TTL succès 180 j, TTL échecs 30 j\n- **Tiles OSM** : cache 3 mois\n\n---\n\n### 7. CHAMPS COLLECTÉS MAIS NON AFFICHÉS\n\n**Colonnes dans `listings` collectées mais rarement utilisées côté frontend** :\n- `details.* json structure` : parking, appliances, contact (téléphone/email) — affichés dans chips/inclusions, pas exploités en recherche avancée\n- `digest.incoherences` : incohérences détectées (prix contradictoires, dates absurdes) — loggé mais pas affiché\n- `digest.confiance` : scores de confiance extraction (haute/faible par champ) — calculé, pas utilisé\n- `img_audit` : détail contrôle images (URLs retirées, raisons) — traçabilité, pas affiché\n\n**Champs présents mais nulls/peu peuplés** :\n- `pets`, `furnished` : null pour ~40-50 % du parc (absent source)\n- `availability` vs `availability_date` : texte source rarement exact\n- `bedrooms`, `bathrooms` : null si pas extractible (extraction robuste mais imparfaite)\n- `details.contact` : téléphone/email rarissime (données source non structurées)\n\n---\n\n### 8. GÉOSPATIAL (Lat/Lng, Géocodage, Routing)\n\n#### Stockage et indexation\n- **Colonnes** : `listings.lat`, `listings.lng` (REAL, nullable)\n- **Index** : `idx_listings_geo` sur (lat, lng) — utilisé par requêtes bbox (carte 3D)\n- **Clé immeuble** : `coord_key = \"{round(lat, 4)},{round(lng, 4)}\"` (~11 m ≈ unités même immeuble)\n - Jointure `listings.coord_key = kascores.coord_key` (partage KA Scores immeuble)\n - Jointure `listings.coord_key = poi_cache.coord_key` (partage POI)\n\n#### Géocodage\n- **Primaire** : Nominatim (OSM) — gratuit, 1 req/sec\n- **Secondaire** : Adresses Québec (MERN) — rues récentes\n- **Cache** : `geocode_cache` (provider, lat, lng, failed flag)\n- **Normalisation clé** : adresse sans numéro app (regex ligne 56 geocode.py)\n- **Validation** : coords source rejetées si hors bounding box province\n\n#### Distance (Haversine, pas routing)\n- **Fonction** : `_haversine_m(lat1, lng1, lat2, lng2)` (poi.py ligne 63-68, kascores.py, tal.py)\n- **Formula** : terre sphérique R=6371 km, calcul en mètres\n- **Pas de routage** : vol d'oiseau × 1.3 (facteur réseau usuel — kascores.py ligne 39 DETOUR)\n- **Pas de PostGIS** : calculs Python pure ou SQL simple (`BETWEEN lat±delta, lng±delta`)\n\n#### Index spatial (grille)\n- **KA Scores** : grille de recherche voisinage (kascores.py `Grid` classe, ligne 85-107)\n - Cellule ~0,01° ≈ 1,1 km\n - Recherche dans rayon par balayage cellules voisines\n- **POI** : tuile 0.5° (poi.py ligne 36) → download par tuile, calcul local\n\n#### Pas d'isochrone / réseau de routage\n- Aucune API OSRM/Vroom/Mapbox Directions intégrée\n- Distance à vol d'oiseau uniquement\n\n#### Affichage carte 3D\n- **Framework** : `@groupe-ka/ka-maps` (wrapper Mapbox GL JS)\n- **Clusters** : par prix moyen (front-end)\n- **Sync liste ↔ carte** : une même requête `/api/search` retourne `listings` (page 1-100) + `points` (tous filtrés, format compact)\n\n---\n\n### 9. PHOTOS / IMAGES\n\n#### Stockage\n- **URLs sources** : collectées dans `listings.images` (JSON liste URLs)\n- **Téléchargement** : non — on conserve URLs sources (référence au stockage CDN du gestionnaire)\n- **Proxy** : endpoint `/api/img?u=&w=` rend miniatures WebP (lazy-loading frontend)\n - Widths : 160, 480, 800, 1280 px\n - Compression WebP q=80\n - Cache sur disque `/data/imgcache/_.webp`\n\n#### Qualité\n- **Audit** (`imgcheck.py`) : mort (404/410), minuscule (<250 × 180), petit (<6 Ko), placeholder (hash sha1 répété)\n- **Résultat** : colonne `images_ok` (galerie nettoyée, URLs mortes retirées)\n- **SHA1** : hash contenu image → détection doublons exact + detection placeholders (même image sur N annonces + N adresses)\n- **Cache URL** : table `image_checks` (status ok/dead/small, dimensions, bytes, sha1)\n\n#### Affichage\n- Frontend : galerie native (lightbox balayage + pincement zoom)\n- Fallback : visuel placeholder Lou-Ka si toutes images mortes\n\n---\n\n### 10. ADRESSES\n\n#### Normalisation (`louka/normalize.py` + `louka/schema.py`)\n- **`clean_address()`** : suppression accents, espaces multiples, HTML entities\n- **`clean_city()`** : normalisation communes (Québec vs Quebec, Lévis vs Levis)\n- **`clean_sector()`** : quartiers/arrondissements\n\n#### Clé adresse (déduplication, `dedup.py` ligne 69-79)\n- **`_parse_address(address, city)`** : extrait (clé_adresse, unit_num)\n- Clé = \"civique|rue_significative|ville\" sans accents ni types de voie\n- Rejet types voie : rue, avenue, boulevard, chemin, place, etc. (FR + EN)\n- Unification points cardinaux : Ouest/West/O → \"o\", Est/East/E → \"e\"\n- Unité = numéro surnuméraire ou \"app/apt/suite/bureau\" + numéro\n\n#### Croisement TAL (par adresse normalisée)\n- Adresse recherche : \"no civique rue\" (mots clés, pas phrases guillemets supportées)\n- **TAL lookup** : jointure `tal_lookup.addr_key = clean_address(listings.address)`\n- Vérification : texte intégral décision confirme adresse exacte\n\n---\n\n## RÉSUMÉ VOLUMÉTRIQUE AU 2026-08-24\n\n| Métrique | Valeur | Notes |\n|----------|--------|-------|\n| **Listings actives** | 60 999 | `WHERE active=1` |\n| **Listings publiées** | 59 819 | Passent qualité, `WHERE published=1` |\n| **Listings en quarantaine** | 1 180 | Sous seuil complétude ou hors sujet (stationnement, garage, etc.) |\n| **Sources connecteurs** | 281 | Recensées dans `data/sources.json` |\n| **Connecteurs actifs** | 250 | 221 long terme + 29 court terme |\n| **Historique prix** | 95 767 entrées | 91 302 UIDs distincts (price_log) |\n| **Fair Value estimées** | 45 709 | ~75 % parc couvert |\n| **KA Scores calculés** | 28 438 immeubles | 95,5 % couverture parc |\n| **Géolocalisées** | ~59 000 | 97,4 % (coords lat/lng présentes) |\n| **Loyer médian** | 1 700 $ | Tous types |\n| **Villes couvertes** | 1 233 | Province entière |\n| **Régions** | 11 | (Montréal, Québec, Outaouais, Estrie, Mauricie, Gaspésie, etc.) |\n| **Séjours court terme** | 41 460 | Section `/court-terme` (base louka_ct.db) |\n| **BD image check** | ~100 000 URLs vérifiées | 4-5 % nouvelles par cycle |\n| **Registre TAL** | 57 433 déclarations | Registre-des-loyers.ca géocodées |\n| **TAL décisions** | ~15 000 | Historique tribunal du logement |\n| **BD Inondation** | Entière province | 493 Mo raster |\n| **BD Quartier** | 10 351 DA (aires diffusion) | Polygones 2021, stats, demos, crimes |\n\n---\n\n## DONNÉES NON PÉRENNISÉES (Colonnes Futures Utiles)\n\nPour votre upgrade, **champs existants à exploiter** :\n- `dup_sources` (JSON) : autres sources même annonce → enrichissement \"aussi publiée sur X gestionnaires\"\n- `dauid` : jointure quartier.db existante → accélération peuplage stats quartier\n- `digest` (JSON) : structuration description existante → réutiliser plutôt que refondre\n- `details` (JSON) : inclusions/parking/appliances → exploiter pour filtres avancés client\n\n**Champs à ajouter pour fiches enrichies** :\n- `building_id` (TEXT, fk) : lien vers entité Immeuble future\n- `landlord_id` (TEXT, fk) : lien vers entité Gestionnaire future\n- `price_per_sqft` (REAL) : $/pi² calculé si area_sqft présente (denormalisation utile)\n- `price_history_json` (TEXT JSON) : cache prix 12 mois (denormal. perf)\n- `google_reviews_avg` (REAL), `google_reviews_count` (INT) : avis Google gestionnaire (SerpApi)\n- `alert_recycled` (INT) : 1 si détecté recyclage (même titre, description, photos sur multiples adresses différentes)\n- `quality_score_explainable` (TEXT JSON) : raisons détaillées score complétude (actuellement raw list)\n\n---\n\n## ARCHITECTURE DÉCISIONNELLE\n\n**En place** :\n- ✓ Schéma central único (Listing dataclass → finalize() → normalisation commune)\n- ✓ Circuit breaker connecteurs\n- ✓ Déduplication inter-sources (autorité source)\n- ✓ Contrôle qualité à l'ingestion (completeness + pertinence)\n- ✓ Historique prix (price_log)\n- ✓ Enrichissement par étapes (geocode → poi → quartier → quality → dedup → fairvalue → kascores)\n- ✓ Cache spatial (poi_cache, geocode_cache, env_tiles)\n\n**À implémenter (votre upgrade)** :\n- Entité Immeuble (agrégation unités, metadata immeuble, gestionnaire locataire des lieux)\n- Entité Gestionnaire (profile détaillé, avis Google via SerpApi, coordonnées)\n- Snapshot annonce (archive complète par dates clés = historique détail fiche)\n- Détection annonces recyclées (comparaison texte/images cross-addresses)\n- Coût réel mensuel dénormalisé (prix + hydroest + chauffage-inclus)\n- Prix/pi² calculé (colonne supplémentaire utile pour tri/filter)\n- KA Scores expliquables (détails déjà présents dans JSON `kascores.details`, à afficher)\n\nBonne chance pour l'upgrade! Le repo est très bien structuré — respect sur l'architecture!"}],"totalDurationMs":210412,"totalTokens":106756,"totalToolUseCount":49,"usage":{"input_tokens":5,"cache_creation_input_tokens":6867,"cache_read_input_tokens":84845,"output_tokens":15039,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":6867},"inference_geo":"","iterations":[{"input_tokens":5,"output_tokens":15039,"cache_read_input_tokens":84845,"cache_creation_input_tokens":6867,"cache_creation":{"ephemeral_5m_input_tokens":6867,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"toolStats":{"readCount":29,"searchCount":1,"bashCount":19,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"1f5dd42c-bc14-4997-aeb1-7e405f877f91","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJuc63RggAAzSjYhNof","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22821,"cache_read_input_tokens":40477,"cache_creation":{"ephemeral_5m_input_tokens":22821,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"49031a34-802b-4331-af2c-72b1e0fcceba"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJuc63RggAAzSjYhNof","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS8gQKqAEIERgCKkBJTjXRjTp9Xiy5Yp7KAa/BM3QlAoWn0ZOWc4vO/svvg8EPoGdtEN43v3QL/gSrqE5OxUPtibbgYXhuaHrdLblTMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagBwPiy1AawAQISDJi8JIlPnACw6OggIxoMrMjFWLWO/v7N7ujyIjBsAoeyPQCDTl6+ITQD28XYxGsUTc9xyFRTiNSfaDZ492qDKbA/ae+AoLDkf5gXspkq9gKb/5Tv/Zbj3IgPPI2Qrkc0ZIQjmbCEnovi2orNXlzPbKanRLfjPc6HgquTvQzj7+VvAbJ9zTn9psRgtTxlR9HRsRwM85TfNV9y2M1y3KsIz6QOum3UbwPxxlN9UUQNiLwPTV1l2fNs+KCWdJCRWbsZBhfODSByzpyC+3f69aSJwOZJ0vhvR0fwb2g1Om3vPL07AqFhtznWLRW+CMuFe+Hod9Um+5wh5InpIAY7f5taJmqkvnvZd1nEU1hjTQwOoD8CDcVScvMvQP3JCQT7r7+KTK/vuGNsEgIYZZGfCSApVA5dNb3FmwUe0gcxCMC0ZA9KBr5UteArczpjEjQvD7djUkvlvH1OuHlz3P2gezvuW+Y9qNb65p0skhnG8xY/bURU8VHkC8kxPT5HGec86q/SnAgWwlBxYxprqmaT4xaKHyWkI1x7gARHkNOekUc7qiSK8dSTfu4T6xCzUyLMVJztwyx2srRh5UpKzxzf+ORm0RVipF8U1hgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22821,"cache_read_input_tokens":40477,"cache_creation":{"ephemeral_5m_input_tokens":22821,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"241961fb-1875-4a39-836b-8d83b3ff3e45"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJuc63RggAAzSjYhNof","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SZcwg1UbGq1nd5KCiBhTs9","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22821,"cache_read_input_tokens":40477,"cache_creation":{"ephemeral_5m_input_tokens":22821,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1e220c88-340d-4113-9034-52c7acc6e498"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SZcwg1UbGq1nd5KCiBhTs9","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# db.py : persistance SQLite — upsert avec détection de changements,\n5\t# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n6\t# cache des pages détail, cache de géocodage.\n7\t# -----------------------------------------------------------------------------\n8\tfrom __future__ import annotations\n9\t\n10\timport json\n11\timport sqlite3\n12\timport statistics\n13\timport time\n14\tfrom pathlib import Path\n15\t\n16\tfrom .schema import Listing\n17\t\n18\tDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n19\t\n20\t# Nombre d'exécutions consécutives où une annonce doit être absente de la\n21\t# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\n22\tMISS_GRACE = 2\n23\t\n24\t# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n25\t# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\n26\tDRIFT_RATIO = 0.25\n27\tDRIFT_MIN_BASE = 8\n28\tDRIFT_HISTORY = 5\n29\t\n30\t_SCHEMA = \"\"\"\n31\tCREATE TABLE IF NOT EXISTS listings (\n32\t uid TEXT PRIMARY KEY,\n33\t source TEXT NOT NULL,\n34\t external_id TEXT NOT NULL,\n35\t url TEXT,\n36\t title TEXT,\n37\t address TEXT,\n38\t sector TEXT,\n39\t city TEXT,\n40\t unit_type TEXT,\n41\t bedrooms REAL,\n42\t bathrooms REAL,\n43\t price REAL,\n44\t price_label TEXT,\n45\t availability TEXT,\n46\t availability_date TEXT,\n47\t area_sqft REAL,\n48\t pets TEXT,\n49\t furnished INTEGER,\n50\t description TEXT,\n51\t amenities TEXT, -- JSON (liste de textes source)\n52\t details TEXT, -- JSON (champs structurés : inclusions, parking…)\n53\t images TEXT, -- JSON\n54\t lat REAL,\n55\t lng REAL,\n56\t geocode_failed INTEGER DEFAULT 0,\n57\t content_hash TEXT,\n58\t first_seen REAL,\n59\t last_seen REAL,\n60\t updated_at REAL,\n61\t miss_count INTEGER DEFAULT 0,\n62\t active INTEGER DEFAULT 1\n63\t);\n64\tCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\n65\tCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\n66\tCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\n67\tCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n68\t\n69\tCREATE TABLE IF NOT EXISTS sync_log (\n70\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n71\t source TEXT,\n72\t ts REAL,\n73\t found INTEGER,\n74\t added INTEGER,\n75\t updated INTEGER,\n76\t removed INTEGER,\n77\t ok INTEGER,\n78\t message TEXT,\n79\t stats TEXT -- JSON : taux de champs null, missed, alerte…\n80\t);\n81\t\n82\tCREATE TABLE IF NOT EXISTS detail_cache (\n83\t source TEXT NOT NULL,\n84\t external_id TEXT NOT NULL,\n85\t key TEXT, -- hash du contenu « liste » de l'annonce\n86\t payload TEXT, -- JSON opaque propre au connecteur\n87\t fetched_at REAL,\n88\t PRIMARY KEY (source, external_id)\n89\t);\n90\t\n91\tCREATE TABLE IF NOT EXISTS price_log (\n92\t uid TEXT NOT NULL,\n93\t ts REAL NOT NULL,\n94\t price REAL -- prix observé (NULL = retiré de l'affichage)\n95\t);\n96\tCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n97\t\n98\tCREATE TABLE IF NOT EXISTS poi_cache (\n99\t coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n100\t lat REAL,\n101\t lng REAL,\n102\t pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n103\t ts REAL\n104\t);\n105\t\n106\tCREATE TABLE IF NOT EXISTS geocode_cache (\n107\t address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n108\t lat REAL,\n109\t lng REAL,\n110\t provider TEXT,\n111\t failed INTEGER DEFAULT 0,\n112\t ts REAL\n113\t);\n114\t\n115\tCREATE TABLE IF NOT EXISTS users (\n116\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n117\t google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n118\t ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n119\t email TEXT,\n120\t name TEXT,\n121\t picture TEXT, -- URL de l'avatar Google\n122\t public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n123\t role TEXT, -- « locataire » | « gestionnaire » | NULL\n124\t org_source TEXT, -- source réclamée (gestionnaire)\n125\t created_at REAL,\n126\t last_login REAL\n127\t);\n128\t\n129\tCREATE TABLE IF NOT EXISTS favorites (\n130\t user_id INTEGER NOT NULL,\n131\t uid TEXT NOT NULL, -- listings.uid\n132\t ts REAL,\n133\t PRIMARY KEY (user_id, uid)\n134\t);\n135\t\n136\tCREATE TABLE IF NOT EXISTS image_checks (\n137\t url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n138\t status TEXT, -- ok | dead | small | error\n139\t width INTEGER,\n140\t height INTEGER,\n141\t bytes INTEGER,\n142\t sha1 TEXT, -- hash du contenu (doublons / placeholders)\n143\t checked_at REAL\n144\t);\n145\t\n146\tCREATE TABLE IF NOT EXISTS env_tiles (\n147\t tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n148\t data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n149\t fetched_at REAL\n150\t);\n151\t\n152\tCREATE TABLE IF NOT EXISTS kascores (\n153\t coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n154\t lat REAL, lng REAL,\n155\t walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n156\t transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n157\t bike REAL,\n158\t calme REAL,\n159\t services REAL,\n160\t global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n161\t details TEXT, -- JSON : détail par score (fiche + méthodologie)\n162\t version TEXT, -- version du barème (recalcul si changement)\n163\t computed_at REAL\n164\t);\n165\t\n166\tCREATE TABLE IF NOT EXISTS source_profiles (\n167\t source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n168\t owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n169\t tagline TEXT, -- accroche personnalisée\n170\t description TEXT,\n171\t website TEXT,\n172\t phone TEXT,\n173\t email TEXT,\n174\t logo_url TEXT,\n175\t updated_at REAL\n176\t);\n177\t\"\"\"\n178\t\n179\t# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n180\t_MIGRATIONS = {\n181\t \"listings\": {\n182\t \"availability_date\": \"TEXT\",\n183\t \"area_sqft\": \"REAL\",\n184\t \"pets\": \"TEXT\",\n185\t \"furnished\": \"INTEGER\",\n186\t \"details\": \"TEXT\",\n187\t \"geocode_failed\": \"INTEGER DEFAULT 0\",\n188\t \"miss_count\": \"INTEGER DEFAULT 0\",\n189\t \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n190\t \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n191\t \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n192\t \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n193\t \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n194\t \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n195\t \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n196\t \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n197\t \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n198\t \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n199\t \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n200\t \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n201\t },\n202\t \"sync_log\": {\n203\t \"stats\": \"TEXT\",\n204\t },\n205\t \"users\": {\n206\t \"ka_id\": \"TEXT\",\n207\t \"public\": \"INTEGER DEFAULT 0\",\n208\t \"role\": \"TEXT\",\n209\t \"org_source\": \"TEXT\",\n210\t \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n211\t \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n212\t \"bio\": \"TEXT\",\n213\t \"city\": \"TEXT\",\n214\t \"phone\": \"TEXT\",\n215\t \"website\": \"TEXT\",\n216\t \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n217\t },\n218\t}\n219\t\n220\t\n221\tdef connect() -> sqlite3.Connection:\n222\t DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n223\t con = sqlite3.connect(DB_PATH)\n224\t con.row_factory = sqlite3.Row\n225\t con.executescript(_SCHEMA)\n226\t for table, cols in _MIGRATIONS.items():\n227\t existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n228\t for col, decl in cols.items():\n229\t if col not in existing:\n230\t con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n231\t # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n232\t # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n233\t con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n234\t con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n235\t \" ON listings(active, published)\")\n236\t con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n237\t \" ON listings(coord_key)\")\n238\t con.commit()\n239\t # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n240\t # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n241\t con.execute(\"PRAGMA journal_mode=WAL\")\n242\t con.execute(\"PRAGMA busy_timeout=30000\")\n243\t return con\n244\t\n245\t\n246\t# ---------------------------------------------------------------------------\n247\t# Synchronisation d'une source\n248\t# ---------------------------------------------------------------------------\n249\t\n250\tdef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n251\t null_price_rate: float) -> str | None:\n252\t \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n253\t\n254\t Retourne un message d'alerte, ou None si tout est normal.\n255\t \"\"\"\n256\t hist = con.execute(\n257\t \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n258\t \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n259\t if len(hist) < 3:\n260\t return None\n261\t med_found = statistics.median(r[\"found\"] for r in hist)\n262\t if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n263\t return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n264\t f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n265\t if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n266\t rates = []\n267\t for r in hist:\n268\t try:\n269\t rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n270\t except (KeyError, ValueError, TypeError):\n271\t continue\n272\t if rates and statistics.median(rates) <= 0.3:\n273\t return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n274\t f\"(habituellement {statistics.median(rates):.0%}) — \"\n275\t \"le format de la source a probablement changé\")\n276\t return None\n277\t\n278\t\n279\tdef sync_source(con: sqlite3.Connection, source: str,\n280\t listings: list[Listing]) -> dict:\n281\t \"\"\"Synchronise les annonces d'une source.\n282\t\n283\t - nouvelle annonce -> insertion\n284\t - annonce modifiée -> mise à jour (comparaison de content_hash)\n285\t - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n286\t exécutions consécutives (délai de grâce)\n287\t - dérive détectée -> alerte consignée, retraits suspendus\n288\t \"\"\"\n289\t now = time.time()\n290\t added = updated = 0\n291\t seen_uids = set()\n292\t\n293\t n = len(listings)\n294\t null_price = sum(1 for l in listings if l.price is None)\n295\t null_addr = sum(1 for l in listings if not l.address)\n296\t null_price_rate = round(null_price / n, 3) if n else 0.0\n297\t\n298\t alert = _drift_alert(con, source, n, null_price_rate)\n299\t\n300\t for lst in listings:\n301\t seen_uids.add(lst.uid)\n302\t h = lst.content_hash()\n303\t row = con.execute(\n304\t \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n305\t (lst.uid,)).fetchone()\n306\t # score de complétude + seuil de publication (quarantaine sous le seuil)\n307\t from .quality import evaluate\n308\t q_score, q_pub, q_reasons = evaluate({\n309\t \"title\": lst.title,\n310\t \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n311\t \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n312\t \"description\": lst.description, \"images\": lst.images,\n313\t \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n314\t \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n315\t \"availability_date\": lst.availability_date,\n316\t \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n317\t \"details\": lst.details, \"url\": lst.url})\n318\t params = dict(\n319\t uid=lst.uid, source=lst.source, external_id=lst.external_id,\n320\t url=lst.url, title=lst.title, address=lst.address,\n321\t sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n322\t bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n323\t price=lst.price, price_label=lst.price_label,\n324\t availability=lst.availability,\n325\t availability_date=lst.availability_date,\n326\t area_sqft=lst.area_sqft, pets=lst.pets,\n327\t furnished=(None if lst.furnished is None else int(lst.furnished)),\n328\t description=lst.description,\n329\t digest=(json.dumps(lst.digest, ensure_ascii=False)\n330\t if getattr(lst, \"digest\", None) else None),\n331\t amenities=json.dumps(lst.amenities, ensure_ascii=False),\n332\t details=json.dumps(lst.details, ensure_ascii=False),\n333\t images=json.dumps(lst.images, ensure_ascii=False),\n334\t lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n335\t completeness=q_score, published=int(q_pub),\n336\t quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n337\t if q_reasons else None),\n338\t )\n339\t if row is None:\n340\t con.execute(\n341\t \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n342\t address, sector, city, unit_type, bedrooms, bathrooms,\n343\t price, price_label,\n344\t availability, availability_date, area_sqft, pets, furnished,\n345\t description, digest, amenities, details, images, lat, lng,\n346\t content_hash, first_seen, last_seen, updated_at,\n347\t miss_count, active, completeness, published,\n348\t quality_reasons)\n349\t VALUES (:uid,:source,:external_id,:url,:title,:address,\n350\t :sector,:city,:unit_type,:bedrooms,:bathrooms,\n351\t :price,:price_label,:availability,\n352\t :availability_date,:area_sqft,:pets,:furnished,\n353\t :description,:digest,:amenities,:details,:images,:lat,:lng,\n354\t :content_hash,:now,:now,:now,0,1,\n355\t :completeness,:published,:quality_reasons)\"\"\", params)\n356\t if lst.price is not None: # prix initial = point de départ de l'historique\n357\t con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n358\t (lst.uid, now, lst.price))\n359\t added += 1\n360\t elif row[\"content_hash\"] != h:\n361\t # COALESCE : ne jamais écraser des coordonnées géocodées par null\n362\t con.execute(\n363\t \"\"\"UPDATE listings SET url=:url, title=:title,\n364\t address=:address, sector=:sector, city=:city,\n365\t unit_type=:unit_type, bedrooms=:bedrooms,\n366\t bathrooms=:bathrooms, price=:price,\n367\t price_label=:price_label, availability=:availability,\n368\t availability_date=:availability_date,\n369\t area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n370\t description=:description, digest=:digest,\n371\t amenities=:amenities, details=:details, images=:images,\n372\t lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n373\t content_hash=:content_hash, last_seen=:now,\n374\t updated_at=:now, miss_count=0, active=1,\n375\t completeness=:completeness, published=:published,\n376\t quality_reasons=:quality_reasons\n377\t WHERE uid=:uid\"\"\", params)\n378\t if (row[\"images\"] or \"[]\") != params[\"images\"]:\n379\t # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n380\t con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n381\t \" WHERE uid=?\", (lst.uid,))\n382\t if lst.price != row[\"price\"]: # changement de prix -> historique\n383\t con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n384\t (lst.uid, now, lst.price))\n385\t updated += 1\n386\t else:\n387\t con.execute(\n388\t \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n389\t (now, lst.uid))\n390\t\n391\t # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n392\t # puis désactivation. Suspendu si une dérive est détectée.\n393\t removed = missed = 0\n394\t if not alert:\n395\t for r in con.execute(\n396\t \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n397\t (source,)).fetchall():\n398\t if r[\"uid\"] in seen_uids:\n399\t continue\n400\t missed += 1\n401\t if r[\"miss_count\"] + 1 >= MISS_GRACE:\n402\t con.execute(\n403\t \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n404\t \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n405\t removed += 1\n406\t else:\n407\t con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n408\t (r[\"uid\"],))\n409\t\n410\t stats = {\n411\t \"null_price_rate\": null_price_rate,\n412\t \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n413\t \"missed\": missed,\n414\t }\n415\t if alert:\n416\t stats[\"alert\"] = alert\n417\t con.execute(\n418\t \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n419\t \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n420\t (source, now, n, added, updated, removed, alert or \"ok\",\n421\t json.dumps(stats, ensure_ascii=False)))\n422\t con.commit()\n423\t out = {\"source\": source, \"found\": n, \"added\": added,\n424\t \"updated\": updated, \"removed\": removed}\n425\t if alert:\n426\t out[\"alert\"] = alert\n427\t return out\n428\t\n429\t\n430\tdef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n431\t con.execute(\n432\t \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n433\t \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n434\t con.commit()\n435\t\n436\t\n437\t# ---------------------------------------------------------------------------\n438\t# Cache des pages détail (« détail si nouveau/modifié »)\n439\t# ---------------------------------------------------------------------------\n440\t\n441\tdef get_cached_detail(con: sqlite3.Connection, source: str,\n442\t external_id: str, key: str) -> dict | None:\n443\t \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n444\t row = con.execute(\n445\t \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n446\t (source, external_id)).fetchone()\n447\t if row and row[\"key\"] == key and row[\"payload\"]:\n448\t try:\n449\t return json.loads(row[\"payload\"])\n450\t except ValueError:\n451\t return None\n452\t return None\n453\t\n454\t\n455\tdef put_cached_detail(con: sqlite3.Connection, source: str,\n456\t external_id: str, key: str, payload: dict) -> None:\n457\t con.execute(\n458\t \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n459\t \" VALUES (?,?,?,?,?)\"\n460\t \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n461\t \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n462\t (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n463\t time.time()))\n464\t con.commit()\n465\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fe8ce03d-aa6b-42e1-ab0e-d8aca92f3744","timestamp":"2026-08-24T21:48:49.895Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","numLines":465,"startLine":1,"totalLines":465}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJuc63RggAAzSjYhNof","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014QKUk3L3UqFt1tzp9uSPDK","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/dedup.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22821,"cache_read_input_tokens":40477,"cache_creation":{"ephemeral_5m_input_tokens":22821,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9089d1d1-eb62-4b89-90eb-00537c801985"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014QKUk3L3UqFt1tzp9uSPDK","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# dedup.py : déduplication INTER-SOURCES — une même unité publiée sur plusieurs\n5\t# plateformes (ex. un gestionnaire agrégé en direct ET via LogisQuébec,\n6\t# ou un particulier sur Kijiji ET Facebook) ne doit apparaître qu'une\n7\t# fois côté site.\n8\t#\n9\t# Principe (recalculé intégralement à chaque passe, idempotent) :\n10\t# 1. Blocage : regrouper les annonces actives par proximité (grille lat/lng\n11\t# ~110 m + cases voisines) OU par clé d'adresse normalisée (n° civique +\n12\t# rue sans type de voie + ville).\n13\t# 2. Confirmation d'une paire : même type d'unité (si connu des deux côtés),\n14\t# prix à ±4 % (si connu), et signal géographique/adresse concordant.\n15\t# 3. Résolution : le membre de la source la PLUS autoritaire devient canonique\n16\t# (dup_of=NULL) ; les autres pointent vers lui (dup_of=uid canonique). Le\n17\t# canonique liste les autres plateformes dans dup_sources (badge « aussi\n18\t# publiée sur… »). Rien n'est supprimé.\n19\t#\n20\t# Le web (web.py) n'affiche que les annonces canoniques : `WHERE dup_of IS NULL`.\n21\t# Appelé par ingest.watch() APRÈS le géocodage (les coordonnées sont l'ingrédient\n22\t# principal du blocage).\n23\t# -----------------------------------------------------------------------------\n24\tfrom __future__ import annotations\n25\t\n26\timport json\n27\t\n28\tfrom . import db\n29\tfrom .normalize import strip_accents\n30\t\n31\t# Autorité des sources : plus l'indice est BAS, plus la source est prioritaire\n32\t# comme canonique. 0 = gestionnaire/courtier en direct (donnée d'origine),\n33\t# 10 = portail agrégateur, 20 = petites annonces / particuliers.\n34\tPORTALS = {\"logisquebec\", \"louer_ca\", \"rentals_ca\", \"zumper\", \"padmapper\",\n35\t \"realtor\", \"mercini\"}\n36\tCLASSIFIEDS = {\"kijiji\", \"lespac\", \"fb_marketplace\", \"bons_locataires\",\n37\t \"roomies\", \"duproprio\", \"oklouer\"}\n38\t\n39\t\n40\tdef _authority(source: str) -> int:\n41\t if source in CLASSIFIEDS:\n42\t return 20\n43\t if source in PORTALS:\n44\t return 10\n45\t return 0 # tout connecteur de gestionnaire/courtier en direct\n46\t\n47\t\n48\t# types de voie retirés pour comparer les rues (fr + en)\n49\t_STREET_TYPES = {\n50\t \"rue\", \"avenue\", \"av\", \"ave\", \"boulevard\", \"boul\", \"bd\", \"chemin\", \"ch\",\n51\t \"montee\", \"montée\", \"rang\", \"place\", \"pl\", \"cote\", \"côte\", \"impasse\",\n52\t \"terrasse\", \"tsse\", \"allee\", \"allée\", \"croissant\", \"crois\", \"cours\",\n53\t \"street\", \"st\", \"road\", \"rd\", \"drive\", \"dr\", \"lane\", \"ln\", \"court\", \"crt\",\n54\t \"way\", \"circle\", \"blvd\", \"de\", \"du\", \"des\", \"la\", \"le\", \"les\", \"l\", \"d\",\n55\t}\n56\t\n57\t\n58\t_UNIT_MARKERS = {\"app\", \"apt\", \"appartement\", \"unit\", \"unite\", \"suite\", \"bureau\"}\n59\t# points cardinaux unifiés FR/EN (« Sherbrooke Ouest » == « Sherbrooke West »)\n60\t_DIRECTIONS = {\"ouest\": \"o\", \"west\": \"o\", \"w\": \"o\", \"est\": \"e\", \"east\": \"e\",\n61\t \"e\": \"e\", \"nord\": \"n\", \"north\": \"n\", \"n\": \"n\", \"sud\": \"s\",\n62\t \"south\": \"s\", \"s\": \"s\"}\n63\t# marqueurs de fin de rue : province/pays (PAS les noms de ville — « Sherbrooke »\n64\t# est aussi un nom de rue fréquent ; le nom de la ville de l'annonce est géré\n65\t# séparément)\n66\t_ADDR_STOP = {\"qc\", \"quebec\", \"canada\", \"que\", \"ca\"}\n67\t\n68\t\n69\tdef _parse_address(address: str, city: str) -> tuple[str | None, str | None]:\n70\t \"\"\"Retourne (clé d'adresse, numéro d'unité) à partir d'une adresse civique.\n71\t\n72\t - clé = « civique|rue-significative|ville » sans accents ni type de voie,\n73\t points cardinaux unifiés FR/EN ; None si aucun numéro civique fiable.\n74\t - numéro d'unité (app/apt/#/suite ou nombre surnuméraire en tête) : sert à\n75\t NE PAS fusionner deux logements distincts au même immeuble. None si absent.\n76\t\n77\t Gère « 304 4557 Rue Sherbrooke » (304 = unité, 4557 = civique), « 12100,\n78\t rue Rodolphe-Forget » (virgule après le civique) et « 4557 Sherbrooke O ».\n79\t \"\"\"\n80\t a = strip_accents((address or \"\").lower())\n81\t toks = [t for t in _re_split(a) if t] # virgules -> séparateurs\n82\t if not toks:\n83\t return None, None\n84\t\n85\t unit = None\n86\t for i, t in enumerate(toks): # marqueur explicite « app 304 »\n87\t if t in _UNIT_MARKERS and i + 1 < len(toks) and toks[i + 1].isdigit():\n88\t unit = toks[i + 1]\n89\t\n90\t lead_nums = [] # nombres en tête\n91\t j = 0\n92\t while j < len(toks) and toks[j].isdigit():\n93\t lead_nums.append(toks[j])\n94\t j += 1\n95\t if not lead_nums:\n96\t return None, unit\n97\t civic = lead_nums[-1] # le nombre collé à la rue\n98\t if len(lead_nums) > 1 and unit is None:\n99\t unit = lead_nums[0]\n100\t\n101\t c = strip_accents((city or \"\").lower()).strip()\n102\t city_toks = set(_re_split(c))\n103\t words = []\n104\t for t in toks[j:]:\n105\t if t in _ADDR_STOP or t.isdigit(): # province/pays/code postal\n106\t break\n107\t if words and t in city_toks: # ville de l'annonce répétée\n108\t break\n109\t if t in _STREET_TYPES or t in _UNIT_MARKERS:\n110\t continue\n111\t words.append(_DIRECTIONS.get(t, t))\n112\t if not words:\n113\t return None, unit\n114\t return f\"{civic}|{'-'.join(words)}|{c}\", unit\n115\t\n116\t\n117\tdef _re_split(s: str) -> list[str]:\n118\t out, cur = [], []\n119\t for ch in s:\n120\t if ch.isalnum():\n121\t cur.append(ch)\n122\t elif cur:\n123\t out.append(\"\".join(cur))\n124\t cur = []\n125\t if cur:\n126\t out.append(\"\".join(cur))\n127\t return out\n128\t\n129\t\n130\tPRICE_TOL = 0.04 # ±4 % sur le loyer\n131\t\n132\t\n133\tdef _same_listing(x: dict, y: dict) -> bool:\n134\t \"\"\"Confirme que deux annonces (de sources différentes) sont la MÊME unité.\n135\t\n136\t Précision d'abord : on préfère afficher un doublon plutôt que masquer par\n137\t erreur un logement distinct. La confirmation exige une clé d'adresse\n138\t identique (même numéro civique + même rue + même ville) — la proximité GPS\n139\t seule est rejetée car elle fusionnait des immeubles voisins différents.\n140\t \"\"\"\n141\t # clé d'adresse civique identique : condition nécessaire\n142\t if not (x[\"akey\"] and y[\"akey\"] and x[\"akey\"] == y[\"akey\"]):\n143\t return False\n144\t # numéros d'unité connus et DIFFÉRENTS -> logements distincts du même\n145\t # immeuble : ne pas fusionner\n146\t if x[\"unit\"] and y[\"unit\"] and x[\"unit\"] != y[\"unit\"]:\n147\t return False\n148\t # type d'unité : s'il est connu des deux côtés, il doit concorder\n149\t ux, uy = x[\"unit_type\"], y[\"unit_type\"]\n150\t if ux and uy and ux != uy:\n151\t return False\n152\t # prix : s'il est connu des deux côtés, tolérance ±4 %\n153\t px, py = x[\"price\"], y[\"price\"]\n154\t if px and py and abs(px - py) > PRICE_TOL * max(px, py):\n155\t return False\n156\t # à ce stade : même adresse civique, unité/type/prix compatibles.\n157\t # exiger un second signal concordant (type OU prix) pour éviter de fusionner\n158\t # deux logements différents partageant seulement l'adresse (plex, tour)\n159\t if ux and uy and ux == uy:\n160\t return True\n161\t if px and py and abs(px - py) <= PRICE_TOL * max(px, py):\n162\t return True\n163\t if x[\"unit\"] and y[\"unit\"] and x[\"unit\"] == y[\"unit\"]:\n164\t return True\n165\t return False\n166\t\n167\t\n168\tdef run(con=None) -> dict:\n169\t \"\"\"Recalcule les groupes de doublons inter-sources. Idempotent.\"\"\"\n170\t own = con is None\n171\t if own:\n172\t con = db.connect()\n173\t rows = con.execute(\n174\t \"SELECT uid, source, external_id, unit_type, price, address, city,\"\n175\t \" lat, lng, images, description, published FROM listings\"\n176\t \" WHERE active=1\").fetchall()\n177\t\n178\t items = []\n179\t by_addr: dict[str, list[int]] = {}\n180\t for r in rows:\n181\t akey, unit = _parse_address(r[\"address\"], r[\"city\"])\n182\t it = {\n183\t \"uid\": r[\"uid\"], \"source\": r[\"source\"], \"unit_type\": r[\"unit_type\"] or \"\",\n184\t \"price\": r[\"price\"], \"akey\": akey, \"unit\": unit,\n185\t \"auth\": _authority(r[\"source\"]),\n186\t \"published\": r[\"published\"] if r[\"published\"] is not None else 1,\n187\t \"richness\": len(r[\"description\"] or \"\") + 50 * _img_count(r[\"images\"]),\n188\t }\n189\t idx = len(items)\n190\t items.append(it)\n191\t if akey:\n192\t by_addr.setdefault(akey, []).append(idx)\n193\t\n194\t # union-find sur les paires confirmées ; un groupe ne peut PAS contenir deux\n195\t # annonces de la même source (une source ne publie pas deux fois la même\n196\t # unité) : cette contrainte empêche l'enchaînement transitif qui fusionnait\n197\t # des logements distincts d'un même immeuble.\n198\t parent = list(range(len(items)))\n199\t srcset: list[set[str]] = [{it[\"source\"]} for it in items]\n200\t\n201\t def find(i):\n202\t while parent[i] != i:\n203\t parent[i] = parent[parent[i]]\n204\t i = parent[i]\n205\t return i\n206\t\n207\t def union(i, j) -> bool:\n208\t ri, rj = find(i), find(j)\n209\t if ri == rj:\n210\t return False\n211\t if srcset[ri] & srcset[rj]: # sources en conflit : refuser la fusion\n212\t return False\n213\t parent[ri] = rj\n214\t srcset[rj] |= srcset[ri]\n215\t return True\n216\t\n217\t def consider(cand: list[int]):\n218\t n = len(cand)\n219\t if n < 2 or n > 400: # garde-fou : blocs trop denses ignorés\n220\t return\n221\t for a in range(n):\n222\t for b in range(a + 1, n):\n223\t ia, ib = cand[a], cand[b]\n224\t if items[ia][\"source\"] == items[ib][\"source\"]:\n225\t continue # jamais dédupliquer au sein d'une source\n226\t if find(ia) == find(ib):\n227\t continue\n228\t if _same_listing(items[ia], items[ib]):\n229\t union(ia, ib)\n230\t\n231\t for group in by_addr.values():\n232\t consider(group)\n233\t\n234\t # regrouper et choisir le canonique (autorité, puis richesse, puis uid)\n235\t groups: dict[int, list[int]] = {}\n236\t for i in range(len(items)):\n237\t groups.setdefault(find(i), []).append(i)\n238\t\n239\t updates = []\n240\t n_dupes = n_groups = 0\n241\t for members in groups.values():\n242\t if len(members) < 2:\n243\t updates.append((None, None, items[members[0]][\"uid\"]))\n244\t continue\n245\t # une annonce en quarantaine ne doit jamais masquer un doublon publié\n246\t members.sort(key=lambda i: (-items[i][\"published\"], items[i][\"auth\"],\n247\t -items[i][\"richness\"], items[i][\"uid\"]))\n248\t canon = members[0]\n249\t canon_uid = items[canon][\"uid\"]\n250\t other_sources = sorted({items[i][\"source\"] for i in members[1:]})\n251\t updates.append((None, json.dumps(other_sources, ensure_ascii=False),\n252\t canon_uid))\n253\t for i in members[1:]:\n254\t updates.append((canon_uid, None, items[i][\"uid\"]))\n255\t n_dupes += 1\n256\t n_groups += 1\n257\t\n258\t con.executemany(\n259\t \"UPDATE listings SET dup_of=?, dup_sources=? WHERE uid=?\", updates)\n260\t con.commit()\n261\t if own:\n262\t con.close()\n263\t stats = {\"groups\": n_groups, \"duplicates_hidden\": n_dupes,\n264\t \"listings_scanned\": len(items)}\n265\t return stats\n266\t\n267\t\n268\tdef _img_count(images_json: str | None) -> int:\n269\t try:\n270\t return len(json.loads(images_json or \"[]\"))\n271\t except (ValueError, TypeError):\n272\t return 0\n273\t\n274\t\n275\tif __name__ == \"__main__\":\n276\t print(run())\n277\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2b7d7783-73da-46ce-965d-70067ea82761","timestamp":"2026-08-24T21:48:49.917Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/dedup.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# dedup.py : déduplication INTER-SOURCES — une même unité publiée sur plusieurs\n# plateformes (ex. un gestionnaire agrégé en direct ET via LogisQuébec,\n# ou un particulier sur Kijiji ET Facebook) ne doit apparaître qu'une\n# fois côté site.\n#\n# Principe (recalculé intégralement à chaque passe, idempotent) :\n# 1. Blocage : regrouper les annonces actives par proximité (grille lat/lng\n# ~110 m + cases voisines) OU par clé d'adresse normalisée (n° civique +\n# rue sans type de voie + ville).\n# 2. Confirmation d'une paire : même type d'unité (si connu des deux côtés),\n# prix à ±4 % (si connu), et signal géographique/adresse concordant.\n# 3. Résolution : le membre de la source la PLUS autoritaire devient canonique\n# (dup_of=NULL) ; les autres pointent vers lui (dup_of=uid canonique). Le\n# canonique liste les autres plateformes dans dup_sources (badge « aussi\n# publiée sur… »). Rien n'est supprimé.\n#\n# Le web (web.py) n'affiche que les annonces canoniques : `WHERE dup_of IS NULL`.\n# Appelé par ingest.watch() APRÈS le géocodage (les coordonnées sont l'ingrédient\n# principal du blocage).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\n\nfrom . import db\nfrom .normalize import strip_accents\n\n# Autorité des sources : plus l'indice est BAS, plus la source est prioritaire\n# comme canonique. 0 = gestionnaire/courtier en direct (donnée d'origine),\n# 10 = portail agrégateur, 20 = petites annonces / particuliers.\nPORTALS = {\"logisquebec\", \"louer_ca\", \"rentals_ca\", \"zumper\", \"padmapper\",\n \"realtor\", \"mercini\"}\nCLASSIFIEDS = {\"kijiji\", \"lespac\", \"fb_marketplace\", \"bons_locataires\",\n \"roomies\", \"duproprio\", \"oklouer\"}\n\n\ndef _authority(source: str) -> int:\n if source in CLASSIFIEDS:\n return 20\n if source in PORTALS:\n return 10\n return 0 # tout connecteur de gestionnaire/courtier en direct\n\n\n# types de voie retirés pour comparer les rues (fr + en)\n_STREET_TYPES = {\n \"rue\", \"avenue\", \"av\", \"ave\", \"boulevard\", \"boul\", \"bd\", \"chemin\", \"ch\",\n \"montee\", \"montée\", \"rang\", \"place\", \"pl\", \"cote\", \"côte\", \"impasse\",\n \"terrasse\", \"tsse\", \"allee\", \"allée\", \"croissant\", \"crois\", \"cours\",\n \"street\", \"st\", \"road\", \"rd\", \"drive\", \"dr\", \"lane\", \"ln\", \"court\", \"crt\",\n \"way\", \"circle\", \"blvd\", \"de\", \"du\", \"des\", \"la\", \"le\", \"les\", \"l\", \"d\",\n}\n\n\n_UNIT_MARKERS = {\"app\", \"apt\", \"appartement\", \"unit\", \"unite\", \"suite\", \"bureau\"}\n# points cardinaux unifiés FR/EN (« Sherbrooke Ouest » == « Sherbrooke West »)\n_DIRECTIONS = {\"ouest\": \"o\", \"west\": \"o\", \"w\": \"o\", \"est\": \"e\", \"east\": \"e\",\n \"e\": \"e\", \"nord\": \"n\", \"north\": \"n\", \"n\": \"n\", \"sud\": \"s\",\n \"south\": \"s\", \"s\": \"s\"}\n# marqueurs de fin de rue : province/pays (PAS les noms de ville — « Sherbrooke »\n# est aussi un nom de rue fréquent ; le nom de la ville de l'annonce est géré\n# séparément)\n_ADDR_STOP = {\"qc\", \"quebec\", \"canada\", \"que\", \"ca\"}\n\n\ndef _parse_address(address: str, city: str) -> tuple[str | None, str | None]:\n \"\"\"Retourne (clé d'adresse, numéro d'unité) à partir d'une adresse civique.\n\n - clé = « civique|rue-significative|ville » sans accents ni type de voie,\n points cardinaux unifiés FR/EN ; None si aucun numéro civique fiable.\n - numéro d'unité (app/apt/#/suite ou nombre surnuméraire en tête) : sert à\n NE PAS fusionner deux logements distincts au même immeuble. None si absent.\n\n Gère « 304 4557 Rue Sherbrooke » (304 = unité, 4557 = civique), « 12100,\n rue Rodolphe-Forget » (virgule après le civique) et « 4557 Sherbrooke O ».\n \"\"\"\n a = strip_accents((address or \"\").lower())\n toks = [t for t in _re_split(a) if t] # virgules -> séparateurs\n if not toks:\n return None, None\n\n unit = None\n for i, t in enumerate(toks): # marqueur explicite « app 304 »\n if t in _UNIT_MARKERS and i + 1 < len(toks) and toks[i + 1].isdigit():\n unit = toks[i + 1]\n\n lead_nums = [] # nombres en tête\n j = 0\n while j < len(toks) and toks[j].isdigit():\n lead_nums.append(toks[j])\n j += 1\n if not lead_nums:\n return None, unit\n civic = lead_nums[-1] # le nombre collé à la rue\n if len(lead_nums) > 1 and unit is None:\n unit = lead_nums[0]\n\n c = strip_accents((city or \"\").lower()).strip()\n city_toks = set(_re_split(c))\n words = []\n for t in toks[j:]:\n if t in _ADDR_STOP or t.isdigit(): # province/pays/code postal\n break\n if words and t in city_toks: # ville de l'annonce répétée\n break\n if t in _STREET_TYPES or t in _UNIT_MARKERS:\n continue\n words.append(_DIRECTIONS.get(t, t))\n if not words:\n return None, unit\n return f\"{civic}|{'-'.join(words)}|{c}\", unit\n\n\ndef _re_split(s: str) -> list[str]:\n out, cur = [], []\n for ch in s:\n if ch.isalnum():\n cur.append(ch)\n elif cur:\n out.append(\"\".join(cur))\n cur = []\n if cur:\n out.append(\"\".join(cur))\n return out\n\n\nPRICE_TOL = 0.04 # ±4 % sur le loyer\n\n\ndef _same_listing(x: dict, y: dict) -> bool:\n \"\"\"Confirme que deux annonces (de sources différentes) sont la MÊME unité.\n\n Précision d'abord : on préfère afficher un doublon plutôt que masquer par\n erreur un logement distinct. La confirmation exige une clé d'adresse\n identique (même numéro civique + même rue + même ville) — la proximité GPS\n seule est rejetée car elle fusionnait des immeubles voisins différents.\n \"\"\"\n # clé d'adresse civique identique : condition nécessaire\n if not (x[\"akey\"] and y[\"akey\"] and x[\"akey\"] == y[\"akey\"]):\n return False\n # numéros d'unité connus et DIFFÉRENTS -> logements distincts du même\n # immeuble : ne pas fusionner\n if x[\"unit\"] and y[\"unit\"] and x[\"unit\"] != y[\"unit\"]:\n return False\n # type d'unité : s'il est connu des deux côtés, il doit concorder\n ux, uy = x[\"unit_type\"], y[\"unit_type\"]\n if ux and uy and ux != uy:\n return False\n # prix : s'il est connu des deux côtés, tolérance ±4 %\n px, py = x[\"price\"], y[\"price\"]\n if px and py and abs(px - py) > PRICE_TOL * max(px, py):\n return False\n # à ce stade : même adresse civique, unité/type/prix compatibles.\n # exiger un second signal concordant (type OU prix) pour éviter de fusionner\n # deux logements différents partageant seulement l'adresse (plex, tour)\n if ux and uy and ux == uy:\n return True\n if px and py and abs(px - py) <= PRICE_TOL * max(px, py):\n return True\n if x[\"unit\"] and y[\"unit\"] and x[\"unit\"] == y[\"unit\"]:\n return True\n return False\n\n\ndef run(con=None) -> dict:\n \"\"\"Recalcule les groupes de doublons inter-sources. Idempotent.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n rows = con.execute(\n \"SELECT uid, source, external_id, unit_type, price, address, city,\"\n \" lat, lng, images, description, published FROM listings\"\n \" WHERE active=1\").fetchall()\n\n items = []\n by_addr: dict[str, list[int]] = {}\n for r in rows:\n akey, unit = _parse_address(r[\"address\"], r[\"city\"])\n it = {\n \"uid\": r[\"uid\"], \"source\": r[\"source\"], \"unit_type\": r[\"unit_type\"] or \"\",\n \"price\": r[\"price\"], \"akey\": akey, \"unit\": unit,\n \"auth\": _authority(r[\"source\"]),\n \"published\": r[\"published\"] if r[\"published\"] is not None else 1,\n \"richness\": len(r[\"description\"] or \"\") + 50 * _img_count(r[\"images\"]),\n }\n idx = len(items)\n items.append(it)\n if akey:\n by_addr.setdefault(akey, []).append(idx)\n\n # union-find sur les paires confirmées ; un groupe ne peut PAS contenir deux\n # annonces de la même source (une source ne publie pas deux fois la même\n # unité) : cette contrainte empêche l'enchaînement transitif qui fusionnait\n # des logements distincts d'un même immeuble.\n parent = list(range(len(items)))\n srcset: list[set[str]] = [{it[\"source\"]} for it in items]\n\n def find(i):\n while parent[i] != i:\n parent[i] = parent[parent[i]]\n i = parent[i]\n return i\n\n def union(i, j) -> bool:\n ri, rj = find(i), find(j)\n if ri == rj:\n return False\n if srcset[ri] & srcset[rj]: # sources en conflit : refuser la fusion\n return False\n parent[ri] = rj\n srcset[rj] |= srcset[ri]\n return True\n\n def consider(cand: list[int]):\n n = len(cand)\n if n < 2 or n > 400: # garde-fou : blocs trop denses ignorés\n return\n for a in range(n):\n for b in range(a + 1, n):\n ia, ib = cand[a], cand[b]\n if items[ia][\"source\"] == items[ib][\"source\"]:\n continue # jamais dédupliquer au sein d'une source\n if find(ia) == find(ib):\n continue\n if _same_listing(items[ia], items[ib]):\n union(ia, ib)\n\n for group in by_addr.values():\n consider(group)\n\n # regrouper et choisir le canonique (autorité, puis richesse, puis uid)\n groups: dict[int, list[int]] = {}\n for i in range(len(items)):\n groups.setdefault(find(i), []).append(i)\n\n updates = []\n n_dupes = n_groups = 0\n for members in groups.values():\n if len(members) < 2:\n updates.append((None, None, items[members[0]][\"uid\"]))\n continue\n # une annonce en quarantaine ne doit jamais masquer un doublon publié\n members.sort(key=lambda i: (-items[i][\"published\"], items[i][\"auth\"],\n -items[i][\"richness\"], items[i][\"uid\"]))\n canon = members[0]\n canon_uid = items[canon][\"uid\"]\n other_sources = sorted({items[i][\"source\"] for i in members[1:]})\n updates.append((None, json.dumps(other_sources, ensure_ascii=False),\n canon_uid))\n for i in members[1:]:\n updates.append((canon_uid, None, items[i][\"uid\"]))\n n_dupes += 1\n n_groups += 1\n\n con.executemany(\n \"UPDATE listings SET dup_of=?, dup_sources=? WHERE uid=?\", updates)\n con.commit()\n if own:\n con.close()\n stats = {\"groups\": n_groups, \"duplicates_hidden\": n_dupes,\n \"listings_scanned\": len(items)}\n return stats\n\n\ndef _img_count(images_json: str | None) -> int:\n try:\n return len(json.loads(images_json or \"[]\"))\n except (ValueError, TypeError):\n return 0\n\n\nif __name__ == \"__main__\":\n print(run())\n","numLines":277,"startLine":1,"totalLines":277}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6c11426d-4fb0-4d45-b2d7-7541998dd38d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJx8ddE5EgpzAezpjK7","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISlwMKpwEIERgCKkBy/C7r596JNMtrKc1L+2XIOSo2GGL35gdSq/YOciMNjynYIu1U5i/bMDRkPrHAOJl8ZUYD16p06CqmWGb4xCaSMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHJ+LLUBrABAhIM8QBm/8vj7QmERxWBGgw0WS6ZQbwReAIgZYAiMPADVVD3u8Ev6M3gsaqu831xNKvTiGHRtl6ScNN+hwsSF+VS5sTKrIddK0EnUhDmqiqcASOvDoxp9ZN5C7Q5LcMSg2KImC7/sXA4fgpaiPMUGpxKSZWmFaAvlk9qbliaO6EI38wYT6m+VFuIcaUntrvk12S/w4JjhPgDlTajBu1VN0kIlo5x3t5G+ph2Ed5rAApLiXy4EnoOPe0Kv9hmRHiqOM6/OBx5nCQLuvdnk9OQLbsi1fQ5lEfa/MseCx/P7GRrBuiQht9JV0qaioo2ZRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":16405,"cache_read_input_tokens":63298,"cache_creation":{"ephemeral_5m_input_tokens":16405,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"72a959d0-901a-480f-88b4-0c00dc98fbb5"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJx8ddE5EgpzAezpjK7","type":"message","role":"assistant","content":[{"type":"text","text":"Je poursuis la lecture des fichiers pivots : `ingest.py`, `web.py` (endpoint fiche), `run.py`, structure de `sources.json`, et le frontend `Listing.tsx`."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":16405,"cache_read_input_tokens":63298,"cache_creation":{"ephemeral_5m_input_tokens":16405,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4c4f8c92-9db9-4714-99be-0c8609d03731"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJx8ddE5EgpzAezpjK7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014ruYZ8M5f28Ug22qRNJc7y","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":16405,"cache_read_input_tokens":63298,"cache_creation":{"ephemeral_5m_input_tokens":16405,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"eed7f246-3730-41f5-ad0a-f714428fbac8"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014ruYZ8M5f28Ug22qRNJc7y","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# ingest.py : pipeline d'ingestion — exécute les connecteurs et synchronise\n5\t# la base (ajouts / mises à jour / retraits) = contenu toujours à jour\n6\t# -----------------------------------------------------------------------------\n7\tfrom __future__ import annotations\n8\t\n9\timport sys\n10\timport time\n11\timport traceback\n12\t\n13\tfrom . import db\n14\tfrom .connectors import CONNECTORS\n15\t\n16\t# Disjoncteur (circuit breaker) : une source qui a échoué aux BREAKER_FAILS\n17\t# derniers passages consécutifs est SAUTÉE (les timeouts/retries des sources\n18\t# mortes ralentissaient tout le cycle), puis retentée un passage sur\n19\t# BREAKER_RETRY_EVERY. Les sauts sont consignés dans sync_log (ok=0,\n20\t# message « disjoncteur: … ») — ils comptent donc comme échecs consécutifs,\n21\t# ce qui fait avancer le compteur jusqu'à la prochaine retentative.\n22\tBREAKER_FAILS = 5\n23\tBREAKER_RETRY_EVERY = 5\n24\t\n25\t\n26\tdef _consecutive_failures(con, source: str) -> int:\n27\t \"\"\"Nombre d'entrées sync_log ok=0 consécutives (les plus récentes).\"\"\"\n28\t rows = con.execute(\n29\t \"SELECT ok FROM sync_log WHERE source=? ORDER BY ts DESC LIMIT 60\",\n30\t (source,)).fetchall()\n31\t n = 0\n32\t for r in rows:\n33\t if r[\"ok\"]:\n34\t break\n35\t n += 1\n36\t return n\n37\t\n38\t\n39\tdef run(sources: list[str] | None = None) -> list[dict]:\n40\t \"\"\"Exécute l'ingestion pour toutes les sources (ou celles demandées).\"\"\"\n41\t con = db.connect()\n42\t results = []\n43\t explicit = sources is not None # sync ciblé : ignorer le disjoncteur\n44\t targets = sources or list(CONNECTORS.keys())\n45\t for sid in targets:\n46\t cls = CONNECTORS.get(sid)\n47\t if cls is None:\n48\t print(f\"[lou-ka] connecteur inconnu : {sid}\", file=sys.stderr)\n49\t continue\n50\t if not explicit:\n51\t fails = _consecutive_failures(con, sid)\n52\t if fails >= BREAKER_FAILS and fails % BREAKER_RETRY_EVERY != 0:\n53\t print(f\"[lou-ka] {sid} sauté (disjoncteur : \"\n54\t f\"{fails} échecs consécutifs)\")\n55\t db.log_failure(con, sid,\n56\t f\"disjoncteur: sauté ({fails} échecs consécutifs)\")\n57\t results.append({\"source\": sid, \"skipped\": True, \"fails\": fails})\n58\t continue\n59\t t0 = time.time()\n60\t print(f\"[lou-ka] sync {sid} ...\")\n61\t try:\n62\t listings = cls().fetch()\n63\t finalized, dropped = [], 0\n64\t for lst in listings:\n65\t try:\n66\t finalized.append(lst.finalize())\n67\t except Exception: # une annonce malformée ne bloque pas la source\n68\t dropped += 1\n69\t stats = db.sync_source(con, sid, finalized)\n70\t stats[\"seconds\"] = round(time.time() - t0, 1)\n71\t if dropped:\n72\t stats[\"dropped\"] = dropped\n73\t if stats.get(\"alert\"):\n74\t print(f\"[lou-ka] ⚠ ALERTE {sid} : {stats['alert']}\")\n75\t print(f\"[lou-ka] {stats}\")\n76\t results.append(stats)\n77\t except Exception as exc: # robustesse : une source ne bloque pas les autres\n78\t db.log_failure(con, sid, f\"{exc}\")\n79\t traceback.print_exc()\n80\t results.append({\"source\": sid, \"error\": str(exc)})\n81\t # statistiques du planificateur SQLite : sans elles, les requêtes bbox de\n82\t # la carte (Ka Maps) retombent sur idx_listings_active au lieu du géo-index\n83\t try:\n84\t con.execute(\"PRAGMA optimize\")\n85\t except Exception:\n86\t pass\n87\t con.close()\n88\t return results\n89\t\n90\t\n91\tdef watch(interval_seconds: int = 3600) -> None:\n92\t \"\"\"Boucle de rafraîchissement périodique (pseudo-webhook par sondage).\"\"\"\n93\t while True:\n94\t run()\n95\t try: # géocoder les nouvelles adresses EN LOT (Adresses Québec)\n96\t from . import geocode\n97\t geocode.run_batch()\n98\t except Exception as exc:\n99\t print(f\"[lou-ka] geocode: erreur non bloquante: {exc}\", file=sys.stderr)\n100\t try: # commodités de proximité des nouveaux immeubles (cache aussi)\n101\t from . import poi\n102\t poi.run(limit=80)\n103\t except Exception as exc:\n104\t print(f\"[lou-ka] poi: erreur non bloquante: {exc}\", file=sys.stderr)\n105\t try: # aire de diffusion (stats de quartier) des nouvelles annonces\n106\t from . import quartier\n107\t quartier.enrich()\n108\t except Exception as exc:\n109\t print(f\"[lou-ka] quartier: erreur non bloquante: {exc}\", file=sys.stderr)\n110\t try: # rafraîchir le score de complétude (le géocodage vient de compléter\n111\t # des coordonnées) + publication/quarantaine\n112\t from . import quality\n113\t quality.backfill()\n114\t except Exception as exc:\n115\t print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n116\t try: # contrôle qualité des images, par lots incrémentaux\n117\t from . import imgcheck\n118\t imgcheck.run(1500)\n119\t except Exception as exc:\n120\t print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n121\t try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n122\t from . import dedup\n123\t stats = dedup.run()\n124\t print(f\"[lou-ka] dedup: {stats}\")\n125\t except Exception as exc:\n126\t print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n127\t try: # juste valeur locative (fair value) — après dedup/geocode/quality\n128\t from . import fairvalue\n129\t fairvalue.compute_all()\n130\t except Exception as exc:\n131\t print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n132\t try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n133\t # incrémentaux des nouveaux immeubles — après géocodage\n134\t from . import environment, kascores\n135\t environment.run(limit=4)\n136\t stats_ks = kascores.run()\n137\t print(f\"[lou-ka] kascores: {stats_ks}\")\n138\t except Exception as exc:\n139\t print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n140\t try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n141\t from . import hydro\n142\t hydro.precompute(limit=100, budget=30)\n143\t except Exception as exc:\n144\t print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n145\t try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n146\t # puis backfill des adresses jamais vérifiées (coût borné)\n147\t from . import tal\n148\t tal.precompute(limit=60, budget=600)\n149\t except Exception as exc:\n150\t print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n151\t print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n152\t time.sleep(interval_seconds)\n153\t\n154\t\n155\tif __name__ == \"__main__\":\n156\t run(sys.argv[1:] or None)\n157\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bd860b25-96dc-4cbf-89cd-b98a8e230ad4","timestamp":"2026-08-24T21:48:58.516Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# ingest.py : pipeline d'ingestion — exécute les connecteurs et synchronise\n# la base (ajouts / mises à jour / retraits) = contenu toujours à jour\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport sys\nimport time\nimport traceback\n\nfrom . import db\nfrom .connectors import CONNECTORS\n\n# Disjoncteur (circuit breaker) : une source qui a échoué aux BREAKER_FAILS\n# derniers passages consécutifs est SAUTÉE (les timeouts/retries des sources\n# mortes ralentissaient tout le cycle), puis retentée un passage sur\n# BREAKER_RETRY_EVERY. Les sauts sont consignés dans sync_log (ok=0,\n# message « disjoncteur: … ») — ils comptent donc comme échecs consécutifs,\n# ce qui fait avancer le compteur jusqu'à la prochaine retentative.\nBREAKER_FAILS = 5\nBREAKER_RETRY_EVERY = 5\n\n\ndef _consecutive_failures(con, source: str) -> int:\n \"\"\"Nombre d'entrées sync_log ok=0 consécutives (les plus récentes).\"\"\"\n rows = con.execute(\n \"SELECT ok FROM sync_log WHERE source=? ORDER BY ts DESC LIMIT 60\",\n (source,)).fetchall()\n n = 0\n for r in rows:\n if r[\"ok\"]:\n break\n n += 1\n return n\n\n\ndef run(sources: list[str] | None = None) -> list[dict]:\n \"\"\"Exécute l'ingestion pour toutes les sources (ou celles demandées).\"\"\"\n con = db.connect()\n results = []\n explicit = sources is not None # sync ciblé : ignorer le disjoncteur\n targets = sources or list(CONNECTORS.keys())\n for sid in targets:\n cls = CONNECTORS.get(sid)\n if cls is None:\n print(f\"[lou-ka] connecteur inconnu : {sid}\", file=sys.stderr)\n continue\n if not explicit:\n fails = _consecutive_failures(con, sid)\n if fails >= BREAKER_FAILS and fails % BREAKER_RETRY_EVERY != 0:\n print(f\"[lou-ka] {sid} sauté (disjoncteur : \"\n f\"{fails} échecs consécutifs)\")\n db.log_failure(con, sid,\n f\"disjoncteur: sauté ({fails} échecs consécutifs)\")\n results.append({\"source\": sid, \"skipped\": True, \"fails\": fails})\n continue\n t0 = time.time()\n print(f\"[lou-ka] sync {sid} ...\")\n try:\n listings = cls().fetch()\n finalized, dropped = [], 0\n for lst in listings:\n try:\n finalized.append(lst.finalize())\n except Exception: # une annonce malformée ne bloque pas la source\n dropped += 1\n stats = db.sync_source(con, sid, finalized)\n stats[\"seconds\"] = round(time.time() - t0, 1)\n if dropped:\n stats[\"dropped\"] = dropped\n if stats.get(\"alert\"):\n print(f\"[lou-ka] ⚠ ALERTE {sid} : {stats['alert']}\")\n print(f\"[lou-ka] {stats}\")\n results.append(stats)\n except Exception as exc: # robustesse : une source ne bloque pas les autres\n db.log_failure(con, sid, f\"{exc}\")\n traceback.print_exc()\n results.append({\"source\": sid, \"error\": str(exc)})\n # statistiques du planificateur SQLite : sans elles, les requêtes bbox de\n # la carte (Ka Maps) retombent sur idx_listings_active au lieu du géo-index\n try:\n con.execute(\"PRAGMA optimize\")\n except Exception:\n pass\n con.close()\n return results\n\n\ndef watch(interval_seconds: int = 3600) -> None:\n \"\"\"Boucle de rafraîchissement périodique (pseudo-webhook par sondage).\"\"\"\n while True:\n run()\n try: # géocoder les nouvelles adresses EN LOT (Adresses Québec)\n from . import geocode\n geocode.run_batch()\n except Exception as exc:\n print(f\"[lou-ka] geocode: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # commodités de proximité des nouveaux immeubles (cache aussi)\n from . import poi\n poi.run(limit=80)\n except Exception as exc:\n print(f\"[lou-ka] poi: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # aire de diffusion (stats de quartier) des nouvelles annonces\n from . import quartier\n quartier.enrich()\n except Exception as exc:\n print(f\"[lou-ka] quartier: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # rafraîchir le score de complétude (le géocodage vient de compléter\n # des coordonnées) + publication/quarantaine\n from . import quality\n quality.backfill()\n except Exception as exc:\n print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # contrôle qualité des images, par lots incrémentaux\n from . import imgcheck\n imgcheck.run(1500)\n except Exception as exc:\n print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n from . import dedup\n stats = dedup.run()\n print(f\"[lou-ka] dedup: {stats}\")\n except Exception as exc:\n print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n # incrémentaux des nouveaux immeubles — après géocodage\n from . import environment, kascores\n environment.run(limit=4)\n stats_ks = kascores.run()\n print(f\"[lou-ka] kascores: {stats_ks}\")\n except Exception as exc:\n print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n from . import hydro\n hydro.precompute(limit=100, budget=30)\n except Exception as exc:\n print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n time.sleep(interval_seconds)\n\n\nif __name__ == \"__main__\":\n run(sys.argv[1:] or None)\n","numLines":157,"startLine":1,"totalLines":157}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJx8ddE5EgpzAezpjK7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Lr2vHYCK9sFuHEmfwqqmnL","name":"Bash","input":{"command":"cd ~/apps/lou-ka && head -c 2000 data/sources.json && echo \"---\" && python3 -c \"import json; d=json.load(open('data/sources.json')); print(type(d), len(d)); print(json.dumps(d[0] if isinstance(d,list) else list(d.items())[0], ensure_ascii=False, indent=1))\" && cat .gitignore && cat .env","description":"Structure de sources.json, .gitignore et .env"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":16405,"cache_read_input_tokens":63298,"cache_creation":{"ephemeral_5m_input_tokens":16405,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c0730fe1-c9d5-4814-9dd5-f5e222a75b93"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Lr2vHYCK9sFuHEmfwqqmnL","type":"tool_result","content":"{\n \"_comment\": \"Lou-Ka — Registre des sources (agences/gestionnaires immobiliers, Québec & Lévis). Auteur : Simon-Pierre Boucher — contact@spboucher.ai\",\n \"sources\": [\n {\n \"id\": \"lafrance_mathieu\",\n \"name\": \"Lafrance & Mathieu\",\n \"url\": \"https://lafrance-mathieu.com\",\n \"listing_url\": \"https://lafrance-mathieu.com/louer-appartement-quebec\",\n \"sectors\": \"Beauport, Charlesbourg, Limoilou, St-Roch, Lévis, Val-Bélair, Loretteville, L'Ancienne-Lorette, St-Augustin\",\n \"connector\": \"lafrance_mathieu\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"msi\",\n \"name\": \"MSI Gestion immobilière\",\n \"url\": \"https://www.msimmobiliers.com\",\n \"listing_url\": \"https://www.msimmobiliers.com/appartements-a-louer/quebec\",\n \"sectors\": \"Ville de Québec, Lévis, Limoilou, Beauport, Ste-Foy, Vanier, St-Nicolas\",\n \"connector\": \"msi\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"logisco\",\n \"name\": \"Logisco\",\n \"url\": \"https://logisco.com/fr\",\n \"listing_url\": \"https://logisco.com/fr/appartements-a-louer\",\n \"sectors\": \"Québec (Ste-Foy, Val-Bélair, Loretteville, Vanier, St-Augustin) + Lévis (St-Romuald, St-David, St-Nicolas, Desjardins)\",\n \"connector\": \"logisco\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"laberge\",\n \"name\": \"Groupe Laberge\",\n \"url\": \"https://www.laberge.qc.ca\",\n \"listing_url\": \"https://www.laberge.qc.ca/recherche\",\n \"sectors\": \"Ste-Foy, Beauport, Vanier, L'Ancienne-Lorette, Limoilou\",\n \"connector\": \"laberge\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"cogir\",\n \"name\": \"Cogir Immobilier\",\n \"url\": \"https://www.cogir.net\",\n \"listing_url\": \"https://www.cogir.net/gestion-immeubles-residentiels.html\",\n \"sectors\": \"Québec (Le Bacc, Loretteville, Père-Lelièvre...)\",\n \"connector\": \"cogir\",\n \"status\": \"actif\",\n ---\n 2\n[\n \"_comment\",\n \"Lou-Ka — Registre des sources (agences/gestionnaires immobiliers, Québec & Lévis). Auteur : Simon-Pierre Boucher — contact@spboucher.ai\"\n]\n.env\n.venv/\n__pycache__/\n*.pyc\ndata/louka.db\nfrontend/node_modules/\nfrontend/dist/\n.DS_Store\nreponse.txt\ndata/staging-*.db\ndata/quartier.db\n\n# app iOS native — dépôt séparé (git.spboucher.ai/lou-ka-ios)\n/ios/\ndata/uploads/\ndata/imgcache/\ndata/louka.db-shm\ndata/louka.db-wal\ndata/louka.db.backup-*\nbackup-connectors-*/\nnode_modules/\ndata/imgcache/\ndata/louka.db-shm\ndata/louka.db-wal\ndata/louka.db.backup-*\ndata/louka_ct.db\ndata/louka_ct.db-shm\ndata/louka_ct.db-wal\nfrontend/tsconfig.tsbuildinfo\ndata/rdl.db\ndata/inondation.db\ndata/air.db\ndata/gaz.db\ndata/commerces.db\ndata/transit.db\ndata/hydro.db\ndata/tal.db\n# Lou-Ka — variables d'environnement (NE PAS COMMITTER)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\nFIRECRAWL_API_KEY=fc-8cc44cb683044aee82e8cbad6505a01a\nSCRAPFLY_KEY=scp-live-4678865fffba41809895d31175deccc0\nSESSION_SECRET=2Qqdikx055p3WeWPzqWy-5_JcPZ5DVAMVT3ZUHHuN63OOkY9JAxKzgGvjOtsFg3z\nLOUKA_BASE_URL=https://www.lou-ka.com\nGOOGLE_CLIENT_ID=380896406983-nfgksio5h3galufqetrk60slffte733d.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=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\nKA_SSO_SECRET=f17ecfd8bc06fb3297189e607b6e1d29aecf3f35bf513bfbad0620ed3db653c5\nKA_HUB_URL=https://www.groupe-ka.com\n\n# Apify (acteur maison ka-fb-marketplace) + proxy résidentiel Oxylabs\nAPIFY_TOKEN=apify_api_ptXnhbXohJQn4ATXKMkrXZbP68DUtl2vqgDS\nOXYLABS_PROXY_USER=customer-groupeka_l5ESf\nOXYLABS_PROXY_PASS=xj~zO7+3Hdq_l_\n\n# Bright Data Web Unlocker — acteur ka-apartments-com (apartments.com/Akamai)\nBRIGHTDATA_API_KEY=7396361a-3dff-47f6-b49f-bd8546871215\nBRIGHTDATA_ZONE=web_unlocker1\nHQ_CAPTCHA_KEY=f0e9a45b0c39a8c38e76e0a7b3db765e\nHQ_CAPTCHA_PROVIDER=2captcha","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"566e8826-7eef-477c-a74a-29c55872a64c","timestamp":"2026-08-24T21:49:00.249Z","tool_use_result":{"stdout":"{\n \"_comment\": \"Lou-Ka — Registre des sources (agences/gestionnaires immobiliers, Québec & Lévis). Auteur : Simon-Pierre Boucher — contact@spboucher.ai\",\n \"sources\": [\n {\n \"id\": \"lafrance_mathieu\",\n \"name\": \"Lafrance & Mathieu\",\n \"url\": \"https://lafrance-mathieu.com\",\n \"listing_url\": \"https://lafrance-mathieu.com/louer-appartement-quebec\",\n \"sectors\": \"Beauport, Charlesbourg, Limoilou, St-Roch, Lévis, Val-Bélair, Loretteville, L'Ancienne-Lorette, St-Augustin\",\n \"connector\": \"lafrance_mathieu\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"msi\",\n \"name\": \"MSI Gestion immobilière\",\n \"url\": \"https://www.msimmobiliers.com\",\n \"listing_url\": \"https://www.msimmobiliers.com/appartements-a-louer/quebec\",\n \"sectors\": \"Ville de Québec, Lévis, Limoilou, Beauport, Ste-Foy, Vanier, St-Nicolas\",\n \"connector\": \"msi\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"logisco\",\n \"name\": \"Logisco\",\n \"url\": \"https://logisco.com/fr\",\n \"listing_url\": \"https://logisco.com/fr/appartements-a-louer\",\n \"sectors\": \"Québec (Ste-Foy, Val-Bélair, Loretteville, Vanier, St-Augustin) + Lévis (St-Romuald, St-David, St-Nicolas, Desjardins)\",\n \"connector\": \"logisco\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"laberge\",\n \"name\": \"Groupe Laberge\",\n \"url\": \"https://www.laberge.qc.ca\",\n \"listing_url\": \"https://www.laberge.qc.ca/recherche\",\n \"sectors\": \"Ste-Foy, Beauport, Vanier, L'Ancienne-Lorette, Limoilou\",\n \"connector\": \"laberge\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n },\n {\n \"id\": \"cogir\",\n \"name\": \"Cogir Immobilier\",\n \"url\": \"https://www.cogir.net\",\n \"listing_url\": \"https://www.cogir.net/gestion-immeubles-residentiels.html\",\n \"sectors\": \"Québec (Le Bacc, Loretteville, Père-Lelièvre...)\",\n \"connector\": \"cogir\",\n \"status\": \"actif\",\n ---\n 2\n[\n \"_comment\",\n \"Lou-Ka — Registre des sources (agences/gestionnaires immobiliers, Québec & Lévis). Auteur : Simon-Pierre Boucher — contact@spboucher.ai\"\n]\n.env\n.venv/\n__pycache__/\n*.pyc\ndata/louka.db\nfrontend/node_modules/\nfrontend/dist/\n.DS_Store\nreponse.txt\ndata/staging-*.db\ndata/quartier.db\n\n# app iOS native — dépôt séparé (git.spboucher.ai/lou-ka-ios)\n/ios/\ndata/uploads/\ndata/imgcache/\ndata/louka.db-shm\ndata/louka.db-wal\ndata/louka.db.backup-*\nbackup-connectors-*/\nnode_modules/\ndata/imgcache/\ndata/louka.db-shm\ndata/louka.db-wal\ndata/louka.db.backup-*\ndata/louka_ct.db\ndata/louka_ct.db-shm\ndata/louka_ct.db-wal\nfrontend/tsconfig.tsbuildinfo\ndata/rdl.db\ndata/inondation.db\ndata/air.db\ndata/gaz.db\ndata/commerces.db\ndata/transit.db\ndata/hydro.db\ndata/tal.db\n# Lou-Ka — variables d'environnement (NE PAS COMMITTER)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\nFIRECRAWL_API_KEY=fc-8cc44cb683044aee82e8cbad6505a01a\nSCRAPFLY_KEY=scp-live-4678865fffba41809895d31175deccc0\nSESSION_SECRET=2Qqdikx055p3WeWPzqWy-5_JcPZ5DVAMVT3ZUHHuN63OOkY9JAxKzgGvjOtsFg3z\nLOUKA_BASE_URL=https://www.lou-ka.com\nGOOGLE_CLIENT_ID=380896406983-nfgksio5h3galufqetrk60slffte733d.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=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\nKA_SSO_SECRET=f17ecfd8bc06fb3297189e607b6e1d29aecf3f35bf513bfbad0620ed3db653c5\nKA_HUB_URL=https://www.groupe-ka.com\n\n# Apify (acteur maison ka-fb-marketplace) + proxy résidentiel Oxylabs\nAPIFY_TOKEN=apify_api_ptXnhbXohJQn4ATXKMkrXZbP68DUtl2vqgDS\nOXYLABS_PROXY_USER=customer-groupeka_l5ESf\nOXYLABS_PROXY_PASS=xj~zO7+3Hdq_l_\n\n# Bright Data Web Unlocker — acteur ka-apartments-com (apartments.com/Akamai)\nBRIGHTDATA_API_KEY=7396361a-3dff-47f6-b49f-bd8546871215\nBRIGHTDATA_ZONE=web_unlocker1\nHQ_CAPTCHA_KEY=f0e9a45b0c39a8c38e76e0a7b3db765e\nHQ_CAPTCHA_PROVIDER=2captcha","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"395eef34-54b9-483b-a323-a89208412c28","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJxtPu46YkPm7ve1r1f","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISwgQKpwEIERgCKkCQA/svIcvbQec7iibCKfn9OzR9Haz8Y8KgcouI9F0Kb4lmWmThLfdlCJUN9sKsrpzgwbaganIdfF+8AhJG6hT/Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHU+LLUBrABAhIMGeoITHxRBMOKDTmyGgwCpClIWxJzwm5Uj5AiMEfCqta8kwKftqJTvMIHhHhdNQFKd9oUKODtdcZ/sJFm3LMsH6g4/TPJnVVS/aI7kSrHAomiiWyv7u1aqkJT3A3dIe+BDu62bvKAAOyikJxxB+DTdBQPEgCzERoWuHEyw5fytV39De/qmOVtoRPFztfl4JGmdoTv9vst37vOlOsZjmrpWC2jIr5GX09uAhgAuf82HpQsYggdBmb0o6V0GcBQQBEoQWuoM7016EyEhXBoiavKqyeOJPtTzlOgt4kbs3jenIkdacWB90z1ylwVUyrNO5V7V9qdUX5zx/nAf/V5ySshz4JLVAC2BiHyJp8xx2UMg7h4VGBaG82bff8Y0pdKxzwCMCKVVheTJ3DKLEOo2c1q3TDN3/jbqCW6R9Bcvs4TFa9lZuPNbD1IASnr2l7UqL8S7cvE2pABGXABGY3xVZz1aropzdbBWdn1zge2oRWzephcWQf6YTR3htKvnIqnt8DAvqBzK53B/2f6HtJizIoz6toYPhODWhgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":5630,"cache_read_input_tokens":79703,"cache_creation":{"ephemeral_5m_input_tokens":5630,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"71a8427b-60e8-4734-a5a4-1299be8ff632"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJxtPu46YkPm7ve1r1f","type":"message","role":"assistant","content":[{"type":"text","text":"Je regarde maintenant `web.py` (endpoint fiche + conventions API) et la page `Listing.tsx` côté frontend."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":5630,"cache_read_input_tokens":79703,"cache_creation":{"ephemeral_5m_input_tokens":5630,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cd173ca6-7d38-426d-a1c9-2a816173b779"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJxtPu46YkPm7ve1r1f","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01AZaVBuREMmRkYND3bsQ1Y3","name":"Bash","input":{"command":"cd ~/apps/lou-ka && wc -l louka/web.py frontend/src/pages/Listing.tsx louka/quality.py louka/fairvalue.py louka/hydro.py louka/tal.py louka/rdl.py louka/kascores.py louka/seo.py 2>/dev/null && ls louka/ && ls frontend/src/components/ && ls frontend/src/pages/","description":"Tailles des fichiers clés et inventaire des modules"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":5630,"cache_read_input_tokens":79703,"cache_creation":{"ephemeral_5m_input_tokens":5630,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f701e17d-a1c9-40c0-a5a5-000c8dc4455e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01AZaVBuREMmRkYND3bsQ1Y3","type":"tool_result","content":" 931 louka/web.py\n 556 frontend/src/pages/Listing.tsx\n 267 louka/quality.py\n 481 louka/fairvalue.py\n 435 louka/hydro.py\n 446 louka/tal.py\n 238 louka/rdl.py\n 452 louka/kascores.py\n 887 louka/seo.py\n 4693 total\n__init__.py\n__pycache__\naccounts.py\nair.py\nauth.py\ncommerces.py\nconnectors\ndb.py\ndedup.py\nenvironment.py\nfairvalue.py\nfixtures.py\ngaz.py\ngeocode.py\nhubfav.py\nhubprofile.py\nhydro.py\nimgcheck.py\ningest.py\ninondation.py\nkapdf.py\nkascores.py\nmarketstats.py\nnormalize.py\npdfgen.py\npoi.py\nprofile.py\nquality.py\nquartier.py\nrdl.py\nschema.py\nseo.py\nshortterm\nstatsdash.py\nstatsextra.py\ntal.py\ntextmine.py\nweb.py\nCommercesProches.tsx\nCookieConsent.tsx\nCtFicheMap.tsx\nCtListingCard.tsx\nCtMapView.tsx\nEssenceProche.tsx\nFairValueBadge.tsx\nHistoriqueTAL.tsx\nHydroEstimation.tsx\nIcons.tsx\nKaScoreBadge.tsx\nKaScoresBlock.tsx\nListingCard.tsx\nListingMap3D.tsx\nLogo.tsx\nMapView.tsx\nMetroLignes.tsx\nPager.tsx\nPriceAnalysis.tsx\nQualiteAir.tsx\nQuartierBlock.tsx\nRegistreLoyers.tsx\nRisqueInondation.tsx\nSmartImg.tsx\nBienvenue.tsx\nBot.tsx\nContact.tsx\nCourtTerme.tsx\nCourtTermeFiche.tsx\nFavoris.tsx\nGestion.tsx\nGestionPublic.tsx\nHome.tsx\nJusteValeur.tsx\nKaScores.tsx\nListing.tsx\nPasserelle.tsx\nPrivacy.tsx\nProfile.tsx\nPublicProfile.tsx\nSources.tsx\nStats.tsx\nTerms.tsx\nVille.tsx","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4f8cf138-e4f3-4a14-a9bf-763363ee8495","timestamp":"2026-08-24T21:49:10.223Z","tool_use_result":{"stdout":" 931 louka/web.py\n 556 frontend/src/pages/Listing.tsx\n 267 louka/quality.py\n 481 louka/fairvalue.py\n 435 louka/hydro.py\n 446 louka/tal.py\n 238 louka/rdl.py\n 452 louka/kascores.py\n 887 louka/seo.py\n 4693 total\n__init__.py\n__pycache__\naccounts.py\nair.py\nauth.py\ncommerces.py\nconnectors\ndb.py\ndedup.py\nenvironment.py\nfairvalue.py\nfixtures.py\ngaz.py\ngeocode.py\nhubfav.py\nhubprofile.py\nhydro.py\nimgcheck.py\ningest.py\ninondation.py\nkapdf.py\nkascores.py\nmarketstats.py\nnormalize.py\npdfgen.py\npoi.py\nprofile.py\nquality.py\nquartier.py\nrdl.py\nschema.py\nseo.py\nshortterm\nstatsdash.py\nstatsextra.py\ntal.py\ntextmine.py\nweb.py\nCommercesProches.tsx\nCookieConsent.tsx\nCtFicheMap.tsx\nCtListingCard.tsx\nCtMapView.tsx\nEssenceProche.tsx\nFairValueBadge.tsx\nHistoriqueTAL.tsx\nHydroEstimation.tsx\nIcons.tsx\nKaScoreBadge.tsx\nKaScoresBlock.tsx\nListingCard.tsx\nListingMap3D.tsx\nLogo.tsx\nMapView.tsx\nMetroLignes.tsx\nPager.tsx\nPriceAnalysis.tsx\nQualiteAir.tsx\nQuartierBlock.tsx\nRegistreLoyers.tsx\nRisqueInondation.tsx\nSmartImg.tsx\nBienvenue.tsx\nBot.tsx\nContact.tsx\nCourtTerme.tsx\nCourtTermeFiche.tsx\nFavoris.tsx\nGestion.tsx\nGestionPublic.tsx\nHome.tsx\nJusteValeur.tsx\nKaScores.tsx\nListing.tsx\nPasserelle.tsx\nPrivacy.tsx\nProfile.tsx\nPublicProfile.tsx\nSources.tsx\nStats.tsx\nTerms.tsx\nVille.tsx","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"1354ed85-4f05-4eab-ba59-b1631c250559","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJyd6vPWy3Sp2vnWvQs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DtfY9c1iS8Hjfje4c9HSre","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1327,"cache_read_input_tokens":85333,"cache_creation":{"ephemeral_5m_input_tokens":1327,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"193b0290-6ba2-4363-9c6c-3486d6685e10"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DtfY9c1iS8Hjfje4c9HSre","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# web.py : API FastAPI (JSON) + service du frontend React (frontend/dist)\n5\t# -----------------------------------------------------------------------------\n6\tfrom __future__ import annotations\n7\t\n8\timport json\n9\timport threading\n10\tfrom pathlib import Path\n11\t\n12\tfrom fastapi import BackgroundTasks, Body, FastAPI, HTTPException, Query\n13\tfrom fastapi.middleware.cors import CORSMiddleware\n14\tfrom fastapi.middleware.gzip import GZipMiddleware\n15\tfrom fastapi.responses import FileResponse, RedirectResponse\n16\tfrom fastapi.staticfiles import StaticFiles\n17\t\n18\tfrom . import auth, db, ingest\n19\t\n20\tROOT = Path(__file__).resolve().parent.parent\n21\tSOURCES_PATH = ROOT / \"data\" / \"sources.json\"\n22\tFRONTEND_DIST = ROOT / \"frontend\" / \"dist\"\n23\t\n24\tapp = FastAPI(title=\"Lou-Ka API\", version=\"1.0\",\n25\t description=\"Agrégateur de logements à louer — province de Québec\")\n26\tapp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n27\t allow_methods=[\"*\"], allow_headers=[\"*\"])\n28\tapp.add_middleware(GZipMiddleware, minimum_size=1000)\n29\t\n30\t_sync_lock = threading.Lock()\n31\t\n32\t# comptes utilisateurs (connexion Google) — voir louka/auth.py\n33\tapp.include_router(auth.router)\n34\t\n35\t# rôles, favoris, pages gestionnaires — voir louka/accounts.py\n36\tfrom . import accounts # noqa: E402 (import tardif : évite le cycle web<->accounts)\n37\tapp.include_router(accounts.router)\n38\t\n39\t# personnalisation du profil (bio, réseaux sociaux, photo) — louka/profile.py\n40\tfrom . import profile as user_profile # noqa: E402\n41\tapp.include_router(user_profile.router)\n42\t\n43\t# section « Location court terme » — sous-système isolé (louka/shortterm/)\n44\tfrom .shortterm import web as ct_web # noqa: E402\n45\tapp.include_router(ct_web.router)\n46\t\n47\t# fichiers téléversés (photos de profil) — data/uploads/\n48\t_UPLOADS = ROOT / \"data\" / \"uploads\"\n49\t_UPLOADS.mkdir(parents=True, exist_ok=True)\n50\tapp.mount(\"/uploads\", StaticFiles(directory=_UPLOADS), name=\"uploads\")\n51\t\n52\t\n53\tdef _row_to_dict(row) -> dict:\n54\t d = dict(row)\n55\t d[\"amenities\"] = json.loads(d.get(\"amenities\") or \"[]\")\n56\t d[\"images\"] = json.loads(d.get(\"images\") or \"[]\")\n57\t # galerie nettoyée par le contrôle qualité (imgcheck.py) : liens morts,\n58\t # minuscules, placeholders et doublons retirés — prime sur la galerie brute\n59\t imgs_ok = d.pop(\"images_ok\", None)\n60\t if imgs_ok:\n61\t try:\n62\t cleaned = json.loads(imgs_ok)\n63\t if isinstance(cleaned, list):\n64\t d[\"images\"] = cleaned\n65\t except (ValueError, TypeError):\n66\t pass\n67\t d.pop(\"img_audit\", None)\n68\t d[\"details\"] = json.loads(d.get(\"details\") or \"{}\")\n69\t # autres plateformes où l'annonce est aussi publiée (badge « aussi sur… »)\n70\t if d.get(\"dup_sources\"):\n71\t try:\n72\t d[\"dup_sources\"] = json.loads(d[\"dup_sources\"])\n73\t except (ValueError, TypeError):\n74\t d[\"dup_sources\"] = []\n75\t if d.get(\"furnished\") is not None:\n76\t d[\"furnished\"] = bool(d[\"furnished\"])\n77\t return d\n78\t\n79\t\n80\t@app.get(\"/api/listings\")\n81\tdef list_listings(\n82\t city: str | None = None,\n83\t sector: str | None = None,\n84\t unit_type: str | None = None,\n85\t source: str | None = None,\n86\t price_max: float | None = None,\n87\t price_min: float | None = None,\n88\t pets: str | None = None, # \"oui\" -> oui OU conditions\n89\t furnished: int | None = None, # 1 / 0\n90\t available_by: str | None = None, # ISO : dispo maintenant ou avant date\n91\t area_min: float | None = None, # superficie minimale (pi²)\n92\t q: str | None = None,\n93\t deal: str | None = None, # sous | marche | sur (fair value)\n94\t kascore_min: float | None = None, # KA Score global minimal\n95\t sort: str | None = None, # \"deal\" affaires | \"ka\" KA Score\n96\t active: int = 1,\n97\t limit: int = Query(500, le=2000),\n98\t offset: int = 0,\n99\t):\n100\t con = db.connect()\n101\t sql = (\"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n102\t \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n103\t \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n104\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n105\t \" WHERE dup_of IS NULL\") # doublons masqués\n106\t args: list = []\n107\t if active in (0, 1):\n108\t sql += \" AND active=?\"; args.append(active)\n109\t if active == 1: # quarantaine qualité : jamais dans les résultats publics\n110\t sql += \" AND published=1\"\n111\t if city:\n112\t sql += \" AND city=?\"; args.append(city)\n113\t if sector:\n114\t sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n115\t if unit_type:\n116\t sql += \" AND unit_type=?\"; args.append(unit_type)\n117\t if source:\n118\t sql += \" AND source=?\"; args.append(source)\n119\t if price_max is not None:\n120\t sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n121\t if price_min is not None:\n122\t sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n123\t if pets == \"oui\":\n124\t sql += \" AND pets IN ('oui','conditions')\"\n125\t elif pets:\n126\t sql += \" AND pets=?\"; args.append(pets)\n127\t if furnished in (0, 1):\n128\t sql += \" AND furnished=?\"; args.append(furnished)\n129\t if available_by:\n130\t # « now » trié après les dates ISO : comparaison explicite\n131\t sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n132\t \" AND availability_date<=?))\"\n133\t args.append(available_by)\n134\t if area_min is not None:\n135\t sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n136\t if q:\n137\t sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n138\t args += [f\"%{q}%\"] * 3\n139\t if deal in (\"sous\", \"marche\", \"sur\"):\n140\t sql += \" AND fv.verdict=?\"; args.append(deal)\n141\t if kascore_min is not None:\n142\t sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n143\t args.append(kascore_min)\n144\t total = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n145\t if sort == \"deal\": # meilleures affaires d'abord (écart le plus négatif)\n146\t sql += (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n147\t \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n148\t elif sort == \"ka\": # meilleurs emplacements d'abord (KA Score global)\n149\t sql += (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n150\t \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n151\t elif sort == \"prix\":\n152\t sql += \" ORDER BY price IS NULL, price ASC LIMIT ? OFFSET ?\"\n153\t else: # défaut : les plus récentes d'abord\n154\t sql += \" ORDER BY first_seen DESC, price IS NULL, price ASC LIMIT ? OFFSET ?\"\n155\t args += [limit, offset]\n156\t rows = [_row_to_dict(r) for r in con.execute(sql, args).fetchall()]\n157\t con.close()\n158\t return {\"total\": total, \"count\": len(rows), \"listings\": rows}\n159\t\n160\t\n161\t@app.get(\"/api/listings.geojson\")\n162\tdef listings_geojson(\n163\t city: str | None = None,\n164\t sector: str | None = None,\n165\t unit_type: str | None = None,\n166\t source: str | None = None,\n167\t price_max: float | None = None,\n168\t price_min: float | None = None,\n169\t pets: str | None = None,\n170\t furnished: int | None = None,\n171\t available_by: str | None = None,\n172\t area_min: float | None = None,\n173\t q: str | None = None,\n174\t bbox: str | None = None,\n175\t limit: int = Query(3000, le=8000),\n176\t):\n177\t \"\"\"Annonces géolocalisées au format GeoJSON (mêmes filtres que /api/listings).\n178\t\n179\t Léger par conception : seuls les champs nécessaires aux marqueurs et aux\n180\t fiches de la carte sont inclus (1 photo, pas de description).\n181\t\n182\t Ka Maps : `bbox=ouest,sud,est,nord` restreint au rectangle visible\n183\t (index idx_listings_geo) ; `limit` plafonne la réponse. La collection\n184\t porte deux membres additionnels : `totalGeocoded` (annonces géolocalisées\n185\t correspondant aux filtres) et `totalMatching` (avec ou sans coordonnées),\n186\t pour afficher « N résultats sans position sur la carte ».\n187\t \"\"\"\n188\t con = db.connect()\n189\t sql = (\"SELECT uid, title, address, price, price_label, unit_type,\"\n190\t \" availability_date, source, city, sector, area_sqft, images,\"\n191\t \" images_ok, lat, lng, fv.verdict AS fv_verdict,\"\n192\t \" fv.deviation AS fv_deviation\"\n193\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n194\t \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n195\t \" AND lat IS NOT NULL AND lng IS NOT NULL\")\n196\t args: list = []\n197\t if bbox:\n198\t try:\n199\t west, south, east, north = (float(v) for v in bbox.split(\",\"))\n200\t except ValueError:\n201\t raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n202\t sql += \" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\"\n203\t args += [south, north, west, east]\n204\t if city:\n205\t sql += \" AND city=?\"; args.append(city)\n206\t if sector:\n207\t sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n208\t if unit_type:\n209\t sql += \" AND unit_type=?\"; args.append(unit_type)\n210\t if source:\n211\t sql += \" AND source=?\"; args.append(source)\n212\t if price_max is not None:\n213\t sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n214\t if price_min is not None:\n215\t sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n216\t if pets == \"oui\":\n217\t sql += \" AND pets IN ('oui','conditions')\"\n218\t elif pets:\n219\t sql += \" AND pets=?\"; args.append(pets)\n220\t if furnished in (0, 1):\n221\t sql += \" AND furnished=?\"; args.append(furnished)\n222\t if available_by:\n223\t sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n224\t \" AND availability_date<=?))\"\n225\t args.append(available_by)\n226\t if area_min is not None:\n227\t sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n228\t if q:\n229\t sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n230\t args += [f\"%{q}%\"] * 3\n231\t\n232\t total_geo = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n233\t # Même requête sans l'exigence de coordonnées ni le bbox : mesure les\n234\t # annonces filtrées invisibles sur la carte (affordance « sans position »).\n235\t sql_all = sql.replace(\" AND lat IS NOT NULL AND lng IS NOT NULL\", \"\")\n236\t args_all = list(args)\n237\t if bbox:\n238\t sql_all = sql_all.replace(\" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\", \"\")\n239\t del args_all[0:4]\n240\t total_all = con.execute(f\"SELECT COUNT(*) c FROM ({sql_all})\", args_all).fetchone()[\"c\"]\n241\t\n242\t sql += \" ORDER BY first_seen DESC LIMIT ?\"\n243\t args.append(limit)\n244\t\n245\t features = []\n246\t for r in con.execute(sql, args).fetchall():\n247\t # galerie nettoyée si auditée (même vide : les images brutes sont\n248\t # alors toutes mortes/placeholder — le frontend affiche le visuel\n249\t # de secours Lou-Ka plutôt qu'une image cassée)\n250\t images = json.loads((r[\"images_ok\"] if r[\"images_ok\"] is not None\n251\t else r[\"images\"]) or \"[]\")\n252\t features.append({\n253\t \"type\": \"Feature\",\n254\t \"geometry\": {\"type\": \"Point\", \"coordinates\": [r[\"lng\"], r[\"lat\"]]},\n255\t \"properties\": {\n256\t \"uid\": r[\"uid\"], \"title\": r[\"title\"], \"address\": r[\"address\"],\n257\t \"price\": r[\"price\"],\n258\t \"price_label\": r[\"price_label\"], \"unit_type\": r[\"unit_type\"],\n259\t \"availability_date\": r[\"availability_date\"],\n260\t \"source\": r[\"source\"], \"city\": r[\"city\"], \"sector\": r[\"sector\"],\n261\t \"area_sqft\": r[\"area_sqft\"],\n262\t \"fv_verdict\": r[\"fv_verdict\"], \"fv_deviation\": r[\"fv_deviation\"],\n263\t \"image\": images[0] if images else None,\n264\t },\n265\t })\n266\t con.close()\n267\t return {\"type\": \"FeatureCollection\", \"features\": features,\n268\t \"totalGeocoded\": total_geo, \"totalMatching\": total_all}\n269\t\n270\t\n271\t# --- Recherche unifiée liste + carte -----------------------------------------\n272\t# Une seule vérité partagée : la même requête sert la page de liste ET les\n273\t# points de la carte, dans la même réponse. Le compteur affiché en tête de\n274\t# liste est par construction égal au nombre de points envoyés à la carte.\n275\t\n276\t_SEARCH_SORTS = {\n277\t \"prix\": \" ORDER BY price IS NULL, price ASC\",\n278\t \"prix_desc\": \" ORDER BY price IS NULL, price DESC\",\n279\t \"recent\": \" ORDER BY first_seen DESC, price IS NULL, price ASC\",\n280\t \"deal\": (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n281\t \" price IS NULL, price ASC\"),\n282\t \"ka\": (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n283\t \" price IS NULL, price ASC\"),\n284\t}\n285\t\n286\t# colonnes KA Scores jointes aux annonces (pastilles + tri + fiche)\n287\t_KS_COLS = (\" ks.walk AS ks_walk, ks.transit AS ks_transit,\"\n288\t \" ks.bike AS ks_bike, ks.calme AS ks_calme,\"\n289\t \" ks.services AS ks_services, ks.global AS ks_global\")\n290\t_KS_JOIN = \" LEFT JOIN kascores ks ON ks.coord_key = listings.coord_key\"\n291\t_FV_CODES = {\"sous\": \"s\", \"marche\": \"m\", \"sur\": \"o\"}\n292\t\n293\t\n294\tdef _parse_poly(poly: str) -> list[tuple[float, float]]:\n295\t \"\"\"`poly=lng,lat;lng,lat;…` → liste de sommets (≥ 3) ou HTTP 400.\"\"\"\n296\t try:\n297\t pts = [tuple(float(v) for v in p.split(\",\")) for p in poly.split(\";\") if p]\n298\t except ValueError:\n299\t raise HTTPException(400, \"poly attendu : lng,lat;lng,lat;…\")\n300\t if len(pts) < 3 or any(len(p) != 2 for p in pts):\n301\t raise HTTPException(400, \"poly : au moins 3 sommets lng,lat\")\n302\t return pts # type: ignore[return-value]\n303\t\n304\t\n305\tdef _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n306\t \"\"\"Ray casting — même algorithme que ka-maps (utils/geo.pointInPolygon).\"\"\"\n307\t inside = False\n308\t j = len(ring) - 1\n309\t for i in range(len(ring)):\n310\t xi, yi = ring[i]\n311\t xj, yj = ring[j]\n312\t if (yi > lat) != (yj > lat) and lng < (xj - xi) * (lat - yi) / (yj - yi) + xi:\n313\t inside = not inside\n314\t j = i\n315\t return inside\n316\t\n317\t\n318\t@app.get(\"/api/search\")\n319\tdef search_unified(\n320\t city: str | None = None,\n321\t sector: str | None = None,\n322\t unit_type: str | None = None,\n323\t source: str | None = None,\n324\t price_max: float | None = None,\n325\t price_min: float | None = None,\n326\t pets: str | None = None,\n327\t furnished: int | None = None,\n328\t available_by: str | None = None,\n329\t area_min: float | None = None,\n330\t q: str | None = None,\n331\t deal: str | None = None, # sous | marche | sur (juste valeur)\n332\t bbox: str | None = None, # ouest,sud,est,nord (zone visible carte)\n333\t poly: str | None = None, # lng,lat;… (zone dessinée)\n334\t kascore_min: float | None = None, # KA Score global minimal (0-100)\n335\t sort: str = \"recent\", # recent (défaut) | prix | prix_desc | deal | ka\n336\t page: int = 1,\n337\t page_size: int = Query(20, ge=1, le=100),\n338\t include: str = \"tout\", # tout | liste (points inchangés côté client)\n339\t):\n340\t \"\"\"Recherche synchronisée liste ↔ carte (une réponse = les deux vues).\n341\t\n342\t Retourne `total` (compteur partagé), `listings` (la page demandée) et\n343\t `points` (TOUS les points correspondants, format compact\n344\t [uid, lng, lat, prix, verdict]) — triés selon `sort`, si bien que la\n345\t position d'un point dans `points` donne directement sa page de liste.\n346\t Seules les annonces géolocalisées participent (la règle d'or : chaque\n347\t annonce de la liste est visible sur la carte) ; `unpositioned` compte\n348\t les annonces filtrées sans coordonnées, pour l'afficher honnêtement.\n349\t \"\"\"\n350\t if sort not in _SEARCH_SORTS:\n351\t raise HTTPException(400, f\"sort inconnu : {sort}\")\n352\t ring = _parse_poly(poly) if poly else None\n353\t\n354\t con = db.connect()\n355\t sql = (\" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n356\t \" WHERE dup_of IS NULL AND active=1 AND published=1\"\n357\t \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\")\n358\t args: list = []\n359\t if bbox:\n360\t try:\n361\t west, south, east, north = (float(v) for v in bbox.split(\",\"))\n362\t except ValueError:\n363\t raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n364\t sql += (\" AND listings.lat BETWEEN ? AND ?\"\n365\t \" AND listings.lng BETWEEN ? AND ?\")\n366\t args += [south, north, west, east]\n367\t if ring:\n368\t # préfiltre SQL par l'emprise du polygone, appartenance exacte en aval\n369\t lngs = [p[0] for p in ring]; lats = [p[1] for p in ring]\n370\t sql += (\" AND listings.lat BETWEEN ? AND ?\"\n371\t \" AND listings.lng BETWEEN ? AND ?\")\n372\t args += [min(lats), max(lats), min(lngs), max(lngs)]\n373\t if city:\n374\t sql += \" AND city=?\"; args.append(city)\n375\t if sector:\n376\t sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n377\t if unit_type:\n378\t sql += \" AND unit_type=?\"; args.append(unit_type)\n379\t if source:\n380\t sql += \" AND source=?\"; args.append(source)\n381\t if price_max is not None:\n382\t sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n383\t if price_min is not None:\n384\t sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n385\t if pets == \"oui\":\n386\t sql += \" AND pets IN ('oui','conditions')\"\n387\t elif pets:\n388\t sql += \" AND pets=?\"; args.append(pets)\n389\t if furnished in (0, 1):\n390\t sql += \" AND furnished=?\"; args.append(furnished)\n391\t if available_by:\n392\t sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n393\t \" AND availability_date<=?))\"\n394\t args.append(available_by)\n395\t if area_min is not None:\n396\t sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n397\t if q:\n398\t sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n399\t args += [f\"%{q}%\"] * 3\n400\t if deal in (\"sous\", \"marche\", \"sur\"):\n401\t sql += \" AND fv.verdict=?\"; args.append(deal)\n402\t if kascore_min is not None:\n403\t sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n404\t args.append(kascore_min)\n405\t\n406\t rows = con.execute(\n407\t \"SELECT uid, listings.lng AS lng, listings.lat AS lat, price,\"\n408\t \" fv.verdict AS v\" + sql + _SEARCH_SORTS[sort],\n409\t args).fetchall()\n410\t if ring:\n411\t rows = [r for r in rows if _point_in_poly(r[\"lng\"], r[\"lat\"], ring)]\n412\t total = len(rows)\n413\t\n414\t # points compacts : [uid, lng, lat, prix, verdict] — l'index dans ce\n415\t # tableau détermine la page (points et liste partagent le même tri)\n416\t points = [[r[\"uid\"], round(r[\"lng\"], 6), round(r[\"lat\"], 6), r[\"price\"],\n417\t _FV_CODES.get(r[\"v\"] or \"\", None)] for r in rows]\n418\t\n419\t # page demandée, ramenée dans les bornes si les filtres l'ont dépassée\n420\t last = max(1, -(-total // page_size))\n421\t page = min(max(1, page), last)\n422\t page_uids = [r[\"uid\"] for r in rows[(page - 1) * page_size: page * page_size]]\n423\t\n424\t listings: list[dict] = []\n425\t if page_uids:\n426\t marks = \",\".join(\"?\" * len(page_uids))\n427\t by_uid = {r[\"uid\"]: _row_to_dict(r) for r in con.execute(\n428\t \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n429\t \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n430\t \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n431\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n432\t f\" WHERE uid IN ({marks})\", page_uids).fetchall()}\n433\t listings = [by_uid[u] for u in page_uids if u in by_uid]\n434\t\n435\t # annonces filtrées mais sans coordonnées (affichage honnête, hors carte)\n436\t sql_nogeo = sql.replace(\n437\t \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\",\n438\t \" AND (listings.lat IS NULL OR listings.lng IS NULL)\", 1)\n439\t args_nogeo = list(args)\n440\t for spatial in (bbox, poly):\n441\t if spatial:\n442\t sql_nogeo = sql_nogeo.replace(\n443\t \" AND listings.lat BETWEEN ? AND ?\"\n444\t \" AND listings.lng BETWEEN ? AND ?\", \"\", 1)\n445\t del args_nogeo[0:4]\n446\t unpositioned = con.execute(\"SELECT COUNT(*) c\" + sql_nogeo,\n447\t args_nogeo).fetchone()[\"c\"]\n448\t con.close()\n449\t\n450\t return {\n451\t \"total\": total, \"page\": page, \"page_size\": page_size, \"sort\": sort,\n452\t \"listings\": listings,\n453\t \"points\": points if include != \"liste\" else None,\n454\t \"unpositioned\": unpositioned,\n455\t }\n456\t\n457\t\n458\t# --- Miniatures optimisées (WebP, taille écran) ------------------------------\n459\t# Sert les images des annonces redimensionnées et compressées (perf mobile) :\n460\t# /api/img?u=&w=480. Seules les URLs déjà vérifiées par le contrôle\n461\t# qualité (table image_checks) sont proxifiées — pas de proxy ouvert. Si\n462\t# l'URL n'est pas (encore) connue : 404, le frontend retombe sur l'original.\n463\t_IMG_WIDTHS = (160, 480, 800, 1280)\n464\t_IMG_CACHE = ROOT / \"data\" / \"imgcache\"\n465\t\n466\t\n467\t@app.get(\"/api/img\")\n468\tdef img_thumbnail(u: str, w: int = 480):\n469\t import hashlib\n470\t import requests as _rq\n471\t from fastapi.responses import Response\n472\t w = min(_IMG_WIDTHS, key=lambda x: abs(x - w))\n473\t if not u.startswith((\"http://\", \"https://\")):\n474\t raise HTTPException(400, \"URL invalide\")\n475\t key = hashlib.sha1(u.encode()).hexdigest()\n476\t _IMG_CACHE.mkdir(parents=True, exist_ok=True)\n477\t path = _IMG_CACHE / f\"{key}_{w}.webp\"\n478\t headers = {\"Cache-Control\": \"public, max-age=2592000, immutable\"}\n479\t if path.exists():\n480\t return Response(path.read_bytes(), media_type=\"image/webp\",\n481\t headers=headers)\n482\t con = db.connect()\n483\t known = con.execute(\"SELECT status FROM image_checks WHERE url=?\",\n484\t (u,)).fetchone()\n485\t con.close()\n486\t if known is None or known[\"status\"] not in (\"ok\",):\n487\t raise HTTPException(404, \"image inconnue du contrôle qualité\")\n488\t try:\n489\t resp = _rq.get(u, timeout=12, headers={\"User-Agent\": \"LouKaImg/1.0\"})\n490\t resp.raise_for_status()\n491\t from io import BytesIO\n492\t from PIL import Image, ImageOps\n493\t img = Image.open(BytesIO(resp.content))\n494\t img = ImageOps.exif_transpose(img)\n495\t if img.mode not in (\"RGB\", \"L\"):\n496\t img = img.convert(\"RGB\")\n497\t if img.width > w:\n498\t img = img.resize((w, max(1, round(img.height * w / img.width))),\n499\t Image.LANCZOS)\n500\t buf = BytesIO()\n501\t img.save(buf, \"WEBP\", quality=80, method=4)\n502\t data = buf.getvalue()\n503\t path.write_bytes(data)\n504\t return Response(data, media_type=\"image/webp\", headers=headers)\n505\t except HTTPException:\n506\t raise\n507\t except Exception:\n508\t raise HTTPException(404, \"image irrécupérable\")\n509\t\n510\t\n511\t@app.get(\"/api/listings/{uid}/pdf\")\n512\tdef listing_pdf(uid: str):\n513\t \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n514\t from fastapi.responses import Response\n515\t from . import pdfgen\n516\t data = pdfgen.fiche_pdf(uid)\n517\t if data is None:\n518\t raise HTTPException(404, \"Annonce introuvable\")\n519\t nom = uid.replace(\":\", \"-\")\n520\t return Response(content=data, media_type=\"application/pdf\", headers={\n521\t \"Content-Disposition\": f'attachment; filename=\"louka-fiche-{nom}.pdf\"'})\n522\t\n523\t\n524\t@app.get(\"/api/stats/rapport.pdf\")\n525\tdef rapport_marche_pdf():\n526\t \"\"\"Rapport global du marché locatif (PDF multi-pages).\"\"\"\n527\t from fastapi.responses import Response\n528\t from . import pdfgen\n529\t return Response(content=pdfgen.rapport_pdf(), media_type=\"application/pdf\",\n530\t headers={\"Content-Disposition\":\n531\t 'attachment; filename=\"louka-rapport-marche.pdf\"'})\n532\t\n533\t\n534\t@app.get(\"/api/fairvalue/{uid}\")\n535\tdef fairvalue_detail(uid: str):\n536\t \"\"\"Analyse de prix Lou-Ka : juste valeur, fourchette, confiance,\n537\t distribution du segment (voir louka/fairvalue.py).\"\"\"\n538\t from . import fairvalue\n539\t d = fairvalue.explain(uid)\n540\t if d is None:\n541\t raise HTTPException(404, \"Pas d'estimation pour cette annonce\")\n542\t return d\n543\t\n544\t\n545\t@app.get(\"/api/hydro\")\n546\tdef hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n547\t lat: float | None = None, lng: float | None = None):\n548\t \"\"\"Estimation du coût d'électricité à une adresse (Hydro-Québec).\n549\t Par défaut cache seulement (aucun captcha) ; solve=1 force le calcul.\n550\t uid/lat/lng permettent la lecture cache par annonce et la dérivation du\n551\t code postal (adresses sans CP).\"\"\"\n552\t from . import hydro\n553\t return hydro.estimate(adresse=adresse, uid=uid, lat=lat, lng=lng,\n554\t solve=bool(solve))\n555\t\n556\t\n557\t@app.get(\"/api/commerces\")\n558\tdef commerces_at(lat: float, lng: float):\n559\t \"\"\"Grands commerces + métro/bus les plus proches (Mapbox / OSM).\"\"\"\n560\t from . import commerces\n561\t return commerces.nearby(lat, lng)\n562\t\n563\t\n564\t@app.get(\"/api/air\")\n565\tdef air_at(lat: float, lng: float):\n566\t \"\"\"Qualité de l'air : station RSQAQ la plus proche (MELCCFP).\"\"\"\n567\t from . import air\n568\t d = air.lookup(lat, lng)\n569\t if d is None:\n570\t raise HTTPException(404, \"Base qualité de l'air non disponible\")\n571\t return d\n572\t\n573\t\n574\t@app.get(\"/api/gaz\")\n575\tdef gaz_at(lat: float, lng: float):\n576\t \"\"\"Stations-service à proximité et prix courants (gazquebec.ca).\"\"\"\n577\t from . import gaz\n578\t return gaz.nearby(lat, lng)\n579\t\n580\t\n581\t@app.get(\"/api/inondation\")\n582\tdef inondation_at(lat: float, lng: float):\n583\t \"\"\"Risque d'inondation à l'adresse (BDZI, gouv. du Québec) —\n584\t bloc « Risque d'inondation » de la fiche.\"\"\"\n585\t from . import inondation\n586\t d = inondation.lookup(lat, lng)\n587\t if d is None:\n588\t raise HTTPException(404, \"Base des zones inondables non disponible\")\n589\t return d\n590\t\n591\t\n592\t@app.get(\"/api/rdl\")\n593\tdef rdl_nearby(lat: float, lng: float, radius: int = 600):\n594\t \"\"\"Loyers déclarés au Registre des loyers (registre-des-loyers.ca)\n595\t autour d'un point — bloc « Registre des loyers » de la fiche.\"\"\"\n596\t from . import rdl\n597\t radius = max(100, min(radius, 2000))\n598\t d = rdl.nearby(lat, lng, radius)\n599\t if d is None:\n600\t raise HTTPException(404, \"Registre des loyers non disponible\")\n601\t return d\n602\t\n603\t\n604\t@app.get(\"/api/tal\")\n605\tdef tal_history(address: str, city: str | None = None):\n606\t \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n607\t à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n608\t Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n609\t from . import tal\n610\t return tal.lookup(address, city)\n611\t\n612\t\n613\t@app.get(\"/api/listings/{uid}\")\n614\tdef get_listing(uid: str):\n615\t con = db.connect()\n616\t row = con.execute(\n617\t \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n618\t \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n619\t \" fv.confidence AS fv_confidence\"\n620\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n621\t \" WHERE uid=?\", (uid,)).fetchone()\n622\t d = None\n623\t if row is not None:\n624\t d = _row_to_dict(row)\n625\t # commodités de proximité (cache par immeuble, voir louka/poi.py)\n626\t if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n627\t key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n628\t poi_row = con.execute(\n629\t \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n630\t d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n631\t else:\n632\t d[\"poi\"] = []\n633\t # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n634\t if d.get(\"coord_key\"):\n635\t ks = con.execute(\n636\t \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n637\t \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n638\t (d[\"coord_key\"],)).fetchone()\n639\t if ks is not None:\n640\t d[\"kascores\"] = {\n641\t \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n642\t \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n643\t \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n644\t \"details\": json.loads(ks[\"details\"] or \"{}\"),\n645\t \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n646\t }\n647\t # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n648\t from . import quartier\n649\t dauid = d.get(\"dauid\")\n650\t d[\"quartier\"] = quartier.fiche_quartier(\n651\t d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n652\t dauid if dauid and dauid != \"hors-zone\" else None)\n653\t # description structurée + historique de prix\n654\t d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n655\t d[\"price_history\"] = [dict(r) for r in con.execute(\n656\t \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n657\t (uid,)).fetchall()]\n658\t con.close()\n659\t if d is None:\n660\t raise HTTPException(404, \"Annonce introuvable\")\n661\t return d\n662\t\n663\t\n664\t@app.get(\"/api/kascores/stats\")\n665\tdef kascores_stats():\n666\t \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n667\t from . import kascores\n668\t return kascores.stats()\n669\t\n670\t\n671\t@app.get(\"/api/facets\")\n672\tdef facets(city: str | None = None):\n673\t \"\"\"Valeurs distinctes pour construire les filtres du frontend.\n674\t\n675\t `city` (optionnel) restreint la liste des quartiers à cette ville —\n676\t utilisé par le sélecteur « Quartier » dépendant de « Ville ».\n677\t \"\"\"\n678\t con = db.connect()\n679\t sector_sql = (\"SELECT DISTINCT sector FROM listings\"\n680\t \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n681\t \" AND sector<>''\")\n682\t sector_args: list = []\n683\t if city:\n684\t sector_sql += \" AND city=?\"\n685\t sector_args.append(city)\n686\t out = {\n687\t \"cities\": [r[\"city\"] for r in con.execute(\n688\t \"SELECT DISTINCT city FROM listings WHERE active=1 AND published=1\"\n689\t \" AND dup_of IS NULL AND city<>'' ORDER BY city\")],\n690\t \"sectors\": [r[\"sector\"] for r in con.execute(\n691\t sector_sql + \" ORDER BY sector\", sector_args)],\n692\t \"unit_types\": [r[\"unit_type\"] for r in con.execute(\n693\t \"SELECT DISTINCT unit_type FROM listings WHERE active=1 AND published=1\"\n694\t \" AND dup_of IS NULL AND unit_type<>'' ORDER BY unit_type\")],\n695\t \"sources\": [dict(r) for r in con.execute(\n696\t \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n697\t \" AND dup_of IS NULL GROUP BY source ORDER BY n DESC\")],\n698\t }\n699\t con.close()\n700\t return out\n701\t\n702\t\n703\t@app.get(\"/api/sources\")\n704\tdef sources():\n705\t registry = json.loads(SOURCES_PATH.read_text(encoding=\"utf-8\"))[\"sources\"]\n706\t con = db.connect()\n707\t counts = {r[\"source\"]: r[\"n\"] for r in con.execute(\n708\t \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n709\t \" GROUP BY source\")}\n710\t last = {r[\"source\"]: r[\"ts\"] for r in con.execute(\n711\t \"SELECT source, MAX(ts) ts FROM sync_log WHERE ok=1 GROUP BY source\")}\n712\t con.close()\n713\t for s in registry:\n714\t s[\"active_listings\"] = counts.get(s[\"id\"], 0)\n715\t s[\"last_sync\"] = last.get(s[\"id\"])\n716\t return {\"sources\": registry}\n717\t\n718\t\n719\t# Villes de la grande région de Montréal (le reste de la province est\n720\t# compté dans « autres » — Outaouais, Estrie, Mauricie, Est-du-Québec…)\n721\t_VILLES_GM = (\n722\t \"Montréal\", \"Montréal-Est\", \"Laval\", \"Longueuil\", \"Brossard\", \"Terrebonne\",\n723\t \"Mascouche\", \"Repentigny\", \"Boucherville\", \"Saint-Lambert\", \"Westmount\",\n724\t \"Côte Saint-Luc\", \"Dorval\", \"Pointe-Claire\", \"Verdun\", \"LaSalle\", \"Anjou\",\n725\t \"Saint-Constant\", \"Delson\", \"Candiac\", \"Châteauguay\", \"Chambly\",\n726\t \"Sainte-Julie\", \"Varennes\", \"Belœil\", \"McMasterville\", \"Mont-Saint-Hilaire\",\n727\t \"Saint-Basile-le-Grand\", \"Saint-Bruno-de-Montarville\", \"Boisbriand\",\n728\t \"Richelieu\", \"Beauharnois\", \"Salaberry-de-Valleyfield\", \"Vaudreuil-Dorion\",\n729\t \"Kirkland\", \"Beaconsfield\", \"Dollard-des-Ormeaux\", \"Mont-Royal\", \"Outremont\",\n730\t)\n731\t\n732\t\n733\t@app.get(\"/api/stats\")\n734\tdef stats(syncs_since_h: float | None = None):\n735\t \"\"\"Stats globales + journal de synchronisation.\n736\t\n737\t `syncs_since_h` (optionnel) : fenêtre en heures pour `recent_syncs` — le\n738\t moniteur central (api-ka) polle aux 2 h et les 20 dernières entrées ne\n739\t couvrent que ~11 min d'activité sur ~200 sources (fausses alertes\n740\t « stale »). Avec le paramètre : toutes les entrées des N dernières heures\n741\t (max 4000) ; s'il y en a moins de 20, on retombe sur les 20 dernières\n742\t toutes époques confondues. Sans le paramètre : comportement historique\n743\t inchangé (LIMIT 20).\n744\t \"\"\"\n745\t con = db.connect()\n746\t gm = \",\".join(\"?\" * len(_VILLES_GM))\n747\t row = con.execute(\n748\t f\"\"\"SELECT COUNT(*) total,\n749\t SUM(CASE WHEN city='Québec' THEN 1 ELSE 0 END) quebec,\n750\t SUM(CASE WHEN city='Lévis' THEN 1 ELSE 0 END) levis,\n751\t SUM(CASE WHEN city IN ({gm}) THEN 1 ELSE 0 END) montreal,\n752\t SUM(CASE WHEN city NOT IN ('Québec','Lévis') AND city NOT IN ({gm})\n753\t THEN 1 ELSE 0 END) autres,\n754\t COUNT(DISTINCT source) sources,\n755\t AVG(price) avg_price\n756\t FROM listings WHERE active=1 AND published=1 AND dup_of IS NULL\"\"\",\n757\t list(_VILLES_GM) + list(_VILLES_GM)).fetchone()\n758\t log: list[dict] = []\n759\t if syncs_since_h is not None:\n760\t import time as _time\n761\t cutoff = _time.time() - syncs_since_h * 3600\n762\t log = [dict(r) for r in con.execute(\n763\t \"SELECT * FROM sync_log WHERE ts >= ? ORDER BY ts DESC LIMIT 4000\",\n764\t (cutoff,))]\n765\t if syncs_since_h is None or len(log) < 20:\n766\t log = [dict(r) for r in con.execute(\n767\t \"SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20\")]\n768\t # qualité des données : complétude moyenne + quarantaine (quality.py)\n769\t qual = con.execute(\n770\t \"\"\"SELECT ROUND(AVG(completeness),1) avg_completeness,\n771\t SUM(published=1) publiees, SUM(published=0) quarantaine\n772\t FROM listings WHERE active=1\"\"\"\n773\t ).fetchone()\n774\t con.close()\n775\t return {**dict(row), \"quality\": dict(qual), \"recent_syncs\": log}\n776\t\n777\t\n778\t@app.get(\"/api/stats/detailed\")\n779\tdef stats_detailed():\n780\t \"\"\"Agrégats du marché (source unique : louka/marketstats.py).\"\"\"\n781\t from . import marketstats\n782\t return marketstats.compute()\n783\t\n784\t\n785\t# --- Module Stats commun Groupe KA (contrat frontend/src/ka/stats/SPEC.md) ---\n786\t_PERIODES_VALIDES = {\"auj\", \"7j\", \"30j\", \"3m\", \"6m\", \"12m\", \"annee\", \"tout\"}\n787\t\n788\t\n789\t@app.get(\"/api/stats/dashboard\")\n790\tdef stats_dashboard(period: str = \"30j\",\n791\t from_: str | None = Query(None, alias=\"from\"),\n792\t to: str | None = None):\n793\t \"\"\"Tableau de bord analytique (KPI, séries, répartitions, tableaux…).\"\"\"\n794\t from . import statsdash\n795\t if period not in _PERIODES_VALIDES and not (from_ and to):\n796\t period = \"30j\"\n797\t return statsdash.compute(period, from_, to)\n798\t\n799\t\n800\t@app.get(\"/api/stats/report\")\n801\tdef stats_report(period: str = \"30j\",\n802\t from_: str | None = Query(None, alias=\"from\"),\n803\t to: str | None = None,\n804\t mode: str = \"complet\"):\n805\t \"\"\"Rapport statistique PDF estampillé Groupe-KA — 5 modes :\n806\t complet | synthese | tendances | repartitions | donnees (inconnu → complet).\"\"\"\n807\t from fastapi.responses import Response\n808\t from . import kapdf, statsdash\n809\t if period not in _PERIODES_VALIDES:\n810\t period = \"perso\" if (from_ and to) else \"30j\"\n811\t if mode not in kapdf.REPORT_MODES:\n812\t mode = \"complet\"\n813\t dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n814\t from_, to)\n815\t eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n816\t \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n817\t site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n818\t data = kapdf.GroupeKAReport(site=site, dashboard=dash, mode=mode).build()\n819\t fname = kapdf.filename(\"lou-ka\", period, mode)\n820\t return Response(content=data, media_type=\"application/pdf\",\n821\t headers={\"Content-Disposition\":\n822\t f'attachment; filename=\"{fname}\"'})\n823\t\n824\t\n825\t\n826\t@app.get(\"/api/stats/catalog\")\n827\tdef stats_catalog(period: str = \"30j\",\n828\t from_: str | None = Query(None, alias=\"from\"),\n829\t to: str | None = None):\n830\t \"\"\"v3 — blocs composables pour le constructeur de rapports personnalisés.\"\"\"\n831\t from . import kapdf, statsdash\n832\t if period not in _PERIODES_VALIDES and not (from_ and to):\n833\t period = \"30j\"\n834\t dash = statsdash.compute(period if period in _PERIODES_VALIDES else \"30j\",\n835\t from_, to)\n836\t return {\"updated\": dash.get(\"updated\"), \"period\": dash.get(\"period\"),\n837\t \"blocks\": kapdf.catalog(dash)}\n838\t\n839\t\n840\t@app.post(\"/api/stats/report/custom\")\n841\tdef stats_report_custom(spec: dict = Body(...)):\n842\t \"\"\"v3 — rapport PDF personnalisé : {\"title\", \"period\", \"from\", \"to\",\n843\t \"blocks\": [{\"key\": \"series:…\", \"render\": \"bar\"}, …]} (SPEC.md §3bis).\"\"\"\n844\t from fastapi.responses import Response\n845\t from . import kapdf, statsdash\n846\t period = str(spec.get(\"period\") or \"30j\")\n847\t from_ = str(spec.get(\"from\") or \"\") or None\n848\t to = str(spec.get(\"to\") or \"\") or None\n849\t if period not in _PERIODES_VALIDES:\n850\t period = \"perso\" if (from_ and to) else \"30j\"\n851\t dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n852\t from_, to)\n853\t known = {b[\"key\"] for b in kapdf.catalog(dash)}\n854\t blocks = [b for b in (spec.get(\"blocks\") or [])\n855\t if isinstance(b, dict) and b.get(\"key\") in known][:40]\n856\t if not blocks:\n857\t raise HTTPException(400, \"Aucun bloc valide dans la composition\")\n858\t eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n859\t \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n860\t site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n861\t data = kapdf.GroupeKAReport(\n862\t site=site, dashboard=dash, mode=kapdf.CUSTOM_MODE,\n863\t spec={\"title\": str(spec.get(\"title\") or \"\")[:80], \"blocks\": blocks},\n864\t ).build()\n865\t fname = kapdf.filename(\"lou-ka\", period, kapdf.CUSTOM_MODE)\n866\t return Response(content=data, media_type=\"application/pdf\",\n867\t headers={\"Content-Disposition\":\n868\t f'attachment; filename=\"{fname}\"'})\n869\t\n870\t\n871\t@app.post(\"/api/sync\")\n872\tdef trigger_sync(background: BackgroundTasks, source: str | None = None):\n873\t \"\"\"Déclenche une synchronisation (équivalent d'un webhook entrant).\"\"\"\n874\t def _job():\n875\t with _sync_lock:\n876\t ingest.run([source] if source else None)\n877\t background.add_task(_job)\n878\t return {\"status\": \"démarré\", \"source\": source or \"toutes\"}\n879\t\n880\t\n881\t# --- Frontend React (build Vite) --------------------------------------------\n882\tif FRONTEND_DIST.exists():\n883\t app.mount(\"/assets\", StaticFiles(directory=FRONTEND_DIST / \"assets\"), name=\"assets\")\n884\t\n885\t # référencement : SSR des routes publiques, robots.txt, sitemaps, 410.\n886\t # Inclus AVANT le catch-all — l'ordre d'enregistrement fait foi.\n887\t from . import seo # noqa: E402\n888\t app.include_router(seo.router)\n889\t\n890\t # Documentation utilisateur (frontend/public/doc → dist/doc) — routes\n891\t # explicites AVANT le catch-all SPA pour servir /doc/ comme un index.\n892\t @app.get(\"/doc\")\n893\t def doc_redirect():\n894\t return RedirectResponse(\"/doc/\", status_code=308)\n895\t\n896\t @app.get(\"/doc/\")\n897\t def doc_index():\n898\t return FileResponse(FRONTEND_DIST / \"doc\" / \"index.html\")\n899\t\n900\t @app.middleware(\"http\")\n901\t async def _cache_headers(request, call_next):\n902\t \"\"\"Politique de cache : les bundles hachés (/assets/…) sont immuables,\n903\t mais index.html doit TOUJOURS être revalidé — sinon les navigateurs\n904\t gardent une vieille version de l'app après un déploiement.\"\"\"\n905\t resp = await call_next(request)\n906\t path = request.url.path\n907\t if path.startswith(\"/assets/\"):\n908\t resp.headers[\"Cache-Control\"] = \"public, max-age=31536000, immutable\"\n909\t elif \"text/html\" in (resp.headers.get(\"content-type\") or \"\"):\n910\t resp.headers[\"Cache-Control\"] = \"no-cache\"\n911\t return resp\n912\t\n913\t # routes de l'app rendues uniquement côté client (privées ou volatiles) —\n914\t # tout autre chemin inconnu renvoie index.html avec un statut 404 : le\n915\t # routeur React affiche sa page « introuvable », les moteurs de recherche\n916\t # voient un vrai 404 (fin des soft-404).\n917\t _CLIENT_ROUTES = {\"profil\", \"favoris\", \"gestion\", \"bienvenue\", \"bot\",\n918\t \"contact\", \"juste-valeur\", \"court-terme\"}\n919\t _CLIENT_PREFIXES = (\"u/\", \"passerelle/\", \"court-terme/\")\n920\t\n921\t @app.get(\"/{full_path:path}\")\n922\t def spa(full_path: str):\n923\t target = FRONTEND_DIST / full_path\n924\t if full_path and target.is_file():\n925\t return FileResponse(target)\n926\t known = (full_path in _CLIENT_ROUTES\n927\t or any(full_path == p.rstrip(\"/\") or full_path.startswith(p)\n928\t for p in _CLIENT_PREFIXES))\n929\t return FileResponse(FRONTEND_DIST / \"index.html\",\n930\t status_code=200 if known else 404,\n931\t headers={\"Cache-Control\": \"no-cache\"})\n932\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5bc0893d-ca14-4879-8a44-ce017361ea56","timestamp":"2026-08-24T21:49:14.732Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# web.py : API FastAPI (JSON) + service du frontend React (frontend/dist)\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport threading\nfrom pathlib import Path\n\nfrom fastapi import BackgroundTasks, Body, FastAPI, HTTPException, Query\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.middleware.gzip import GZipMiddleware\nfrom fastapi.responses import FileResponse, RedirectResponse\nfrom fastapi.staticfiles import StaticFiles\n\nfrom . import auth, db, ingest\n\nROOT = Path(__file__).resolve().parent.parent\nSOURCES_PATH = ROOT / \"data\" / \"sources.json\"\nFRONTEND_DIST = ROOT / \"frontend\" / \"dist\"\n\napp = FastAPI(title=\"Lou-Ka API\", version=\"1.0\",\n description=\"Agrégateur de logements à louer — province de Québec\")\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n allow_methods=[\"*\"], allow_headers=[\"*\"])\napp.add_middleware(GZipMiddleware, minimum_size=1000)\n\n_sync_lock = threading.Lock()\n\n# comptes utilisateurs (connexion Google) — voir louka/auth.py\napp.include_router(auth.router)\n\n# rôles, favoris, pages gestionnaires — voir louka/accounts.py\nfrom . import accounts # noqa: E402 (import tardif : évite le cycle web<->accounts)\napp.include_router(accounts.router)\n\n# personnalisation du profil (bio, réseaux sociaux, photo) — louka/profile.py\nfrom . import profile as user_profile # noqa: E402\napp.include_router(user_profile.router)\n\n# section « Location court terme » — sous-système isolé (louka/shortterm/)\nfrom .shortterm import web as ct_web # noqa: E402\napp.include_router(ct_web.router)\n\n# fichiers téléversés (photos de profil) — data/uploads/\n_UPLOADS = ROOT / \"data\" / \"uploads\"\n_UPLOADS.mkdir(parents=True, exist_ok=True)\napp.mount(\"/uploads\", StaticFiles(directory=_UPLOADS), name=\"uploads\")\n\n\ndef _row_to_dict(row) -> dict:\n d = dict(row)\n d[\"amenities\"] = json.loads(d.get(\"amenities\") or \"[]\")\n d[\"images\"] = json.loads(d.get(\"images\") or \"[]\")\n # galerie nettoyée par le contrôle qualité (imgcheck.py) : liens morts,\n # minuscules, placeholders et doublons retirés — prime sur la galerie brute\n imgs_ok = d.pop(\"images_ok\", None)\n if imgs_ok:\n try:\n cleaned = json.loads(imgs_ok)\n if isinstance(cleaned, list):\n d[\"images\"] = cleaned\n except (ValueError, TypeError):\n pass\n d.pop(\"img_audit\", None)\n d[\"details\"] = json.loads(d.get(\"details\") or \"{}\")\n # autres plateformes où l'annonce est aussi publiée (badge « aussi sur… »)\n if d.get(\"dup_sources\"):\n try:\n d[\"dup_sources\"] = json.loads(d[\"dup_sources\"])\n except (ValueError, TypeError):\n d[\"dup_sources\"] = []\n if d.get(\"furnished\") is not None:\n d[\"furnished\"] = bool(d[\"furnished\"])\n return d\n\n\n@app.get(\"/api/listings\")\ndef list_listings(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None, # \"oui\" -> oui OU conditions\n furnished: int | None = None, # 1 / 0\n available_by: str | None = None, # ISO : dispo maintenant ou avant date\n area_min: float | None = None, # superficie minimale (pi²)\n q: str | None = None,\n deal: str | None = None, # sous | marche | sur (fair value)\n kascore_min: float | None = None, # KA Score global minimal\n sort: str | None = None, # \"deal\" affaires | \"ka\" KA Score\n active: int = 1,\n limit: int = Query(500, le=2000),\n offset: int = 0,\n):\n con = db.connect()\n sql = (\"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n \" WHERE dup_of IS NULL\") # doublons masqués\n args: list = []\n if active in (0, 1):\n sql += \" AND active=?\"; args.append(active)\n if active == 1: # quarantaine qualité : jamais dans les résultats publics\n sql += \" AND published=1\"\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n # « now » trié après les dates ISO : comparaison explicite\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n if deal in (\"sous\", \"marche\", \"sur\"):\n sql += \" AND fv.verdict=?\"; args.append(deal)\n if kascore_min is not None:\n sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n args.append(kascore_min)\n total = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n if sort == \"deal\": # meilleures affaires d'abord (écart le plus négatif)\n sql += (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n elif sort == \"ka\": # meilleurs emplacements d'abord (KA Score global)\n sql += (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n elif sort == \"prix\":\n sql += \" ORDER BY price IS NULL, price ASC LIMIT ? OFFSET ?\"\n else: # défaut : les plus récentes d'abord\n sql += \" ORDER BY first_seen DESC, price IS NULL, price ASC LIMIT ? OFFSET ?\"\n args += [limit, offset]\n rows = [_row_to_dict(r) for r in con.execute(sql, args).fetchall()]\n con.close()\n return {\"total\": total, \"count\": len(rows), \"listings\": rows}\n\n\n@app.get(\"/api/listings.geojson\")\ndef listings_geojson(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None,\n furnished: int | None = None,\n available_by: str | None = None,\n area_min: float | None = None,\n q: str | None = None,\n bbox: str | None = None,\n limit: int = Query(3000, le=8000),\n):\n \"\"\"Annonces géolocalisées au format GeoJSON (mêmes filtres que /api/listings).\n\n Léger par conception : seuls les champs nécessaires aux marqueurs et aux\n fiches de la carte sont inclus (1 photo, pas de description).\n\n Ka Maps : `bbox=ouest,sud,est,nord` restreint au rectangle visible\n (index idx_listings_geo) ; `limit` plafonne la réponse. La collection\n porte deux membres additionnels : `totalGeocoded` (annonces géolocalisées\n correspondant aux filtres) et `totalMatching` (avec ou sans coordonnées),\n pour afficher « N résultats sans position sur la carte ».\n \"\"\"\n con = db.connect()\n sql = (\"SELECT uid, title, address, price, price_label, unit_type,\"\n \" availability_date, source, city, sector, area_sqft, images,\"\n \" images_ok, lat, lng, fv.verdict AS fv_verdict,\"\n \" fv.deviation AS fv_deviation\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND lat IS NOT NULL AND lng IS NOT NULL\")\n args: list = []\n if bbox:\n try:\n west, south, east, north = (float(v) for v in bbox.split(\",\"))\n except ValueError:\n raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n sql += \" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\"\n args += [south, north, west, east]\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n\n total_geo = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n # Même requête sans l'exigence de coordonnées ni le bbox : mesure les\n # annonces filtrées invisibles sur la carte (affordance « sans position »).\n sql_all = sql.replace(\" AND lat IS NOT NULL AND lng IS NOT NULL\", \"\")\n args_all = list(args)\n if bbox:\n sql_all = sql_all.replace(\" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\", \"\")\n del args_all[0:4]\n total_all = con.execute(f\"SELECT COUNT(*) c FROM ({sql_all})\", args_all).fetchone()[\"c\"]\n\n sql += \" ORDER BY first_seen DESC LIMIT ?\"\n args.append(limit)\n\n features = []\n for r in con.execute(sql, args).fetchall():\n # galerie nettoyée si auditée (même vide : les images brutes sont\n # alors toutes mortes/placeholder — le frontend affiche le visuel\n # de secours Lou-Ka plutôt qu'une image cassée)\n images = json.loads((r[\"images_ok\"] if r[\"images_ok\"] is not None\n else r[\"images\"]) or \"[]\")\n features.append({\n \"type\": \"Feature\",\n \"geometry\": {\"type\": \"Point\", \"coordinates\": [r[\"lng\"], r[\"lat\"]]},\n \"properties\": {\n \"uid\": r[\"uid\"], \"title\": r[\"title\"], \"address\": r[\"address\"],\n \"price\": r[\"price\"],\n \"price_label\": r[\"price_label\"], \"unit_type\": r[\"unit_type\"],\n \"availability_date\": r[\"availability_date\"],\n \"source\": r[\"source\"], \"city\": r[\"city\"], \"sector\": r[\"sector\"],\n \"area_sqft\": r[\"area_sqft\"],\n \"fv_verdict\": r[\"fv_verdict\"], \"fv_deviation\": r[\"fv_deviation\"],\n \"image\": images[0] if images else None,\n },\n })\n con.close()\n return {\"type\": \"FeatureCollection\", \"features\": features,\n \"totalGeocoded\": total_geo, \"totalMatching\": total_all}\n\n\n# --- Recherche unifiée liste + carte -----------------------------------------\n# Une seule vérité partagée : la même requête sert la page de liste ET les\n# points de la carte, dans la même réponse. Le compteur affiché en tête de\n# liste est par construction égal au nombre de points envoyés à la carte.\n\n_SEARCH_SORTS = {\n \"prix\": \" ORDER BY price IS NULL, price ASC\",\n \"prix_desc\": \" ORDER BY price IS NULL, price DESC\",\n \"recent\": \" ORDER BY first_seen DESC, price IS NULL, price ASC\",\n \"deal\": (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n \" price IS NULL, price ASC\"),\n \"ka\": (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n \" price IS NULL, price ASC\"),\n}\n\n# colonnes KA Scores jointes aux annonces (pastilles + tri + fiche)\n_KS_COLS = (\" ks.walk AS ks_walk, ks.transit AS ks_transit,\"\n \" ks.bike AS ks_bike, ks.calme AS ks_calme,\"\n \" ks.services AS ks_services, ks.global AS ks_global\")\n_KS_JOIN = \" LEFT JOIN kascores ks ON ks.coord_key = listings.coord_key\"\n_FV_CODES = {\"sous\": \"s\", \"marche\": \"m\", \"sur\": \"o\"}\n\n\ndef _parse_poly(poly: str) -> list[tuple[float, float]]:\n \"\"\"`poly=lng,lat;lng,lat;…` → liste de sommets (≥ 3) ou HTTP 400.\"\"\"\n try:\n pts = [tuple(float(v) for v in p.split(\",\")) for p in poly.split(\";\") if p]\n except ValueError:\n raise HTTPException(400, \"poly attendu : lng,lat;lng,lat;…\")\n if len(pts) < 3 or any(len(p) != 2 for p in pts):\n raise HTTPException(400, \"poly : au moins 3 sommets lng,lat\")\n return pts # type: ignore[return-value]\n\n\ndef _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n \"\"\"Ray casting — même algorithme que ka-maps (utils/geo.pointInPolygon).\"\"\"\n inside = False\n j = len(ring) - 1\n for i in range(len(ring)):\n xi, yi = ring[i]\n xj, yj = ring[j]\n if (yi > lat) != (yj > lat) and lng < (xj - xi) * (lat - yi) / (yj - yi) + xi:\n inside = not inside\n j = i\n return inside\n\n\n@app.get(\"/api/search\")\ndef search_unified(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None,\n furnished: int | None = None,\n available_by: str | None = None,\n area_min: float | None = None,\n q: str | None = None,\n deal: str | None = None, # sous | marche | sur (juste valeur)\n bbox: str | None = None, # ouest,sud,est,nord (zone visible carte)\n poly: str | None = None, # lng,lat;… (zone dessinée)\n kascore_min: float | None = None, # KA Score global minimal (0-100)\n sort: str = \"recent\", # recent (défaut) | prix | prix_desc | deal | ka\n page: int = 1,\n page_size: int = Query(20, ge=1, le=100),\n include: str = \"tout\", # tout | liste (points inchangés côté client)\n):\n \"\"\"Recherche synchronisée liste ↔ carte (une réponse = les deux vues).\n\n Retourne `total` (compteur partagé), `listings` (la page demandée) et\n `points` (TOUS les points correspondants, format compact\n [uid, lng, lat, prix, verdict]) — triés selon `sort`, si bien que la\n position d'un point dans `points` donne directement sa page de liste.\n Seules les annonces géolocalisées participent (la règle d'or : chaque\n annonce de la liste est visible sur la carte) ; `unpositioned` compte\n les annonces filtrées sans coordonnées, pour l'afficher honnêtement.\n \"\"\"\n if sort not in _SEARCH_SORTS:\n raise HTTPException(400, f\"sort inconnu : {sort}\")\n ring = _parse_poly(poly) if poly else None\n\n con = db.connect()\n sql = (\" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n \" WHERE dup_of IS NULL AND active=1 AND published=1\"\n \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\")\n args: list = []\n if bbox:\n try:\n west, south, east, north = (float(v) for v in bbox.split(\",\"))\n except ValueError:\n raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n sql += (\" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\")\n args += [south, north, west, east]\n if ring:\n # préfiltre SQL par l'emprise du polygone, appartenance exacte en aval\n lngs = [p[0] for p in ring]; lats = [p[1] for p in ring]\n sql += (\" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\")\n args += [min(lats), max(lats), min(lngs), max(lngs)]\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n if deal in (\"sous\", \"marche\", \"sur\"):\n sql += \" AND fv.verdict=?\"; args.append(deal)\n if kascore_min is not None:\n sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n args.append(kascore_min)\n\n rows = con.execute(\n \"SELECT uid, listings.lng AS lng, listings.lat AS lat, price,\"\n \" fv.verdict AS v\" + sql + _SEARCH_SORTS[sort],\n args).fetchall()\n if ring:\n rows = [r for r in rows if _point_in_poly(r[\"lng\"], r[\"lat\"], ring)]\n total = len(rows)\n\n # points compacts : [uid, lng, lat, prix, verdict] — l'index dans ce\n # tableau détermine la page (points et liste partagent le même tri)\n points = [[r[\"uid\"], round(r[\"lng\"], 6), round(r[\"lat\"], 6), r[\"price\"],\n _FV_CODES.get(r[\"v\"] or \"\", None)] for r in rows]\n\n # page demandée, ramenée dans les bornes si les filtres l'ont dépassée\n last = max(1, -(-total // page_size))\n page = min(max(1, page), last)\n page_uids = [r[\"uid\"] for r in rows[(page - 1) * page_size: page * page_size]]\n\n listings: list[dict] = []\n if page_uids:\n marks = \",\".join(\"?\" * len(page_uids))\n by_uid = {r[\"uid\"]: _row_to_dict(r) for r in con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n f\" WHERE uid IN ({marks})\", page_uids).fetchall()}\n listings = [by_uid[u] for u in page_uids if u in by_uid]\n\n # annonces filtrées mais sans coordonnées (affichage honnête, hors carte)\n sql_nogeo = sql.replace(\n \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\",\n \" AND (listings.lat IS NULL OR listings.lng IS NULL)\", 1)\n args_nogeo = list(args)\n for spatial in (bbox, poly):\n if spatial:\n sql_nogeo = sql_nogeo.replace(\n \" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\", \"\", 1)\n del args_nogeo[0:4]\n unpositioned = con.execute(\"SELECT COUNT(*) c\" + sql_nogeo,\n args_nogeo).fetchone()[\"c\"]\n con.close()\n\n return {\n \"total\": total, \"page\": page, \"page_size\": page_size, \"sort\": sort,\n \"listings\": listings,\n \"points\": points if include != \"liste\" else None,\n \"unpositioned\": unpositioned,\n }\n\n\n# --- Miniatures optimisées (WebP, taille écran) ------------------------------\n# Sert les images des annonces redimensionnées et compressées (perf mobile) :\n# /api/img?u=&w=480. Seules les URLs déjà vérifiées par le contrôle\n# qualité (table image_checks) sont proxifiées — pas de proxy ouvert. Si\n# l'URL n'est pas (encore) connue : 404, le frontend retombe sur l'original.\n_IMG_WIDTHS = (160, 480, 800, 1280)\n_IMG_CACHE = ROOT / \"data\" / \"imgcache\"\n\n\n@app.get(\"/api/img\")\ndef img_thumbnail(u: str, w: int = 480):\n import hashlib\n import requests as _rq\n from fastapi.responses import Response\n w = min(_IMG_WIDTHS, key=lambda x: abs(x - w))\n if not u.startswith((\"http://\", \"https://\")):\n raise HTTPException(400, \"URL invalide\")\n key = hashlib.sha1(u.encode()).hexdigest()\n _IMG_CACHE.mkdir(parents=True, exist_ok=True)\n path = _IMG_CACHE / f\"{key}_{w}.webp\"\n headers = {\"Cache-Control\": \"public, max-age=2592000, immutable\"}\n if path.exists():\n return Response(path.read_bytes(), media_type=\"image/webp\",\n headers=headers)\n con = db.connect()\n known = con.execute(\"SELECT status FROM image_checks WHERE url=?\",\n (u,)).fetchone()\n con.close()\n if known is None or known[\"status\"] not in (\"ok\",):\n raise HTTPException(404, \"image inconnue du contrôle qualité\")\n try:\n resp = _rq.get(u, timeout=12, headers={\"User-Agent\": \"LouKaImg/1.0\"})\n resp.raise_for_status()\n from io import BytesIO\n from PIL import Image, ImageOps\n img = Image.open(BytesIO(resp.content))\n img = ImageOps.exif_transpose(img)\n if img.mode not in (\"RGB\", \"L\"):\n img = img.convert(\"RGB\")\n if img.width > w:\n img = img.resize((w, max(1, round(img.height * w / img.width))),\n Image.LANCZOS)\n buf = BytesIO()\n img.save(buf, \"WEBP\", quality=80, method=4)\n data = buf.getvalue()\n path.write_bytes(data)\n return Response(data, media_type=\"image/webp\", headers=headers)\n except HTTPException:\n raise\n except Exception:\n raise HTTPException(404, \"image irrécupérable\")\n\n\n@app.get(\"/api/listings/{uid}/pdf\")\ndef listing_pdf(uid: str):\n \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n from fastapi.responses import Response\n from . import pdfgen\n data = pdfgen.fiche_pdf(uid)\n if data is None:\n raise HTTPException(404, \"Annonce introuvable\")\n nom = uid.replace(\":\", \"-\")\n return Response(content=data, media_type=\"application/pdf\", headers={\n \"Content-Disposition\": f'attachment; filename=\"louka-fiche-{nom}.pdf\"'})\n\n\n@app.get(\"/api/stats/rapport.pdf\")\ndef rapport_marche_pdf():\n \"\"\"Rapport global du marché locatif (PDF multi-pages).\"\"\"\n from fastapi.responses import Response\n from . import pdfgen\n return Response(content=pdfgen.rapport_pdf(), media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n 'attachment; filename=\"louka-rapport-marche.pdf\"'})\n\n\n@app.get(\"/api/fairvalue/{uid}\")\ndef fairvalue_detail(uid: str):\n \"\"\"Analyse de prix Lou-Ka : juste valeur, fourchette, confiance,\n distribution du segment (voir louka/fairvalue.py).\"\"\"\n from . import fairvalue\n d = fairvalue.explain(uid)\n if d is None:\n raise HTTPException(404, \"Pas d'estimation pour cette annonce\")\n return d\n\n\n@app.get(\"/api/hydro\")\ndef hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n lat: float | None = None, lng: float | None = None):\n \"\"\"Estimation du coût d'électricité à une adresse (Hydro-Québec).\n Par défaut cache seulement (aucun captcha) ; solve=1 force le calcul.\n uid/lat/lng permettent la lecture cache par annonce et la dérivation du\n code postal (adresses sans CP).\"\"\"\n from . import hydro\n return hydro.estimate(adresse=adresse, uid=uid, lat=lat, lng=lng,\n solve=bool(solve))\n\n\n@app.get(\"/api/commerces\")\ndef commerces_at(lat: float, lng: float):\n \"\"\"Grands commerces + métro/bus les plus proches (Mapbox / OSM).\"\"\"\n from . import commerces\n return commerces.nearby(lat, lng)\n\n\n@app.get(\"/api/air\")\ndef air_at(lat: float, lng: float):\n \"\"\"Qualité de l'air : station RSQAQ la plus proche (MELCCFP).\"\"\"\n from . import air\n d = air.lookup(lat, lng)\n if d is None:\n raise HTTPException(404, \"Base qualité de l'air non disponible\")\n return d\n\n\n@app.get(\"/api/gaz\")\ndef gaz_at(lat: float, lng: float):\n \"\"\"Stations-service à proximité et prix courants (gazquebec.ca).\"\"\"\n from . import gaz\n return gaz.nearby(lat, lng)\n\n\n@app.get(\"/api/inondation\")\ndef inondation_at(lat: float, lng: float):\n \"\"\"Risque d'inondation à l'adresse (BDZI, gouv. du Québec) —\n bloc « Risque d'inondation » de la fiche.\"\"\"\n from . import inondation\n d = inondation.lookup(lat, lng)\n if d is None:\n raise HTTPException(404, \"Base des zones inondables non disponible\")\n return d\n\n\n@app.get(\"/api/rdl\")\ndef rdl_nearby(lat: float, lng: float, radius: int = 600):\n \"\"\"Loyers déclarés au Registre des loyers (registre-des-loyers.ca)\n autour d'un point — bloc « Registre des loyers » de la fiche.\"\"\"\n from . import rdl\n radius = max(100, min(radius, 2000))\n d = rdl.nearby(lat, lng, radius)\n if d is None:\n raise HTTPException(404, \"Registre des loyers non disponible\")\n return d\n\n\n@app.get(\"/api/tal\")\ndef tal_history(address: str, city: str | None = None):\n \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n from . import tal\n return tal.lookup(address, city)\n\n\n@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):\n con = db.connect()\n row = con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE uid=?\", (uid,)).fetchone()\n d = None\n if row is not None:\n d = _row_to_dict(row)\n # commodités de proximité (cache par immeuble, voir louka/poi.py)\n if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n poi_row = con.execute(\n \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n else:\n d[\"poi\"] = []\n # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n if d.get(\"coord_key\"):\n ks = con.execute(\n \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n (d[\"coord_key\"],)).fetchone()\n if ks is not None:\n d[\"kascores\"] = {\n \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n \"details\": json.loads(ks[\"details\"] or \"{}\"),\n \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n }\n # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n from . import quartier\n dauid = d.get(\"dauid\")\n d[\"quartier\"] = quartier.fiche_quartier(\n d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n d[\"price_history\"] = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n if d is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return d\n\n\n@app.get(\"/api/kascores/stats\")\ndef kascores_stats():\n \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n from . import kascores\n return kascores.stats()\n\n\n@app.get(\"/api/facets\")\ndef facets(city: str | None = None):\n \"\"\"Valeurs distinctes pour construire les filtres du frontend.\n\n `city` (optionnel) restreint la liste des quartiers à cette ville —\n utilisé par le sélecteur « Quartier » dépendant de « Ville ».\n \"\"\"\n con = db.connect()\n sector_sql = (\"SELECT DISTINCT sector FROM listings\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND sector<>''\")\n sector_args: list = []\n if city:\n sector_sql += \" AND city=?\"\n sector_args.append(city)\n out = {\n \"cities\": [r[\"city\"] for r in con.execute(\n \"SELECT DISTINCT city FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL AND city<>'' ORDER BY city\")],\n \"sectors\": [r[\"sector\"] for r in con.execute(\n sector_sql + \" ORDER BY sector\", sector_args)],\n \"unit_types\": [r[\"unit_type\"] for r in con.execute(\n \"SELECT DISTINCT unit_type FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL AND unit_type<>'' ORDER BY unit_type\")],\n \"sources\": [dict(r) for r in con.execute(\n \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL GROUP BY source ORDER BY n DESC\")],\n }\n con.close()\n return out\n\n\n@app.get(\"/api/sources\")\ndef sources():\n registry = json.loads(SOURCES_PATH.read_text(encoding=\"utf-8\"))[\"sources\"]\n con = db.connect()\n counts = {r[\"source\"]: r[\"n\"] for r in con.execute(\n \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n \" GROUP BY source\")}\n last = {r[\"source\"]: r[\"ts\"] for r in con.execute(\n \"SELECT source, MAX(ts) ts FROM sync_log WHERE ok=1 GROUP BY source\")}\n con.close()\n for s in registry:\n s[\"active_listings\"] = counts.get(s[\"id\"], 0)\n s[\"last_sync\"] = last.get(s[\"id\"])\n return {\"sources\": registry}\n\n\n# Villes de la grande région de Montréal (le reste de la province est\n# compté dans « autres » — Outaouais, Estrie, Mauricie, Est-du-Québec…)\n_VILLES_GM = (\n \"Montréal\", \"Montréal-Est\", \"Laval\", \"Longueuil\", \"Brossard\", \"Terrebonne\",\n \"Mascouche\", \"Repentigny\", \"Boucherville\", \"Saint-Lambert\", \"Westmount\",\n \"Côte Saint-Luc\", \"Dorval\", \"Pointe-Claire\", \"Verdun\", \"LaSalle\", \"Anjou\",\n \"Saint-Constant\", \"Delson\", \"Candiac\", \"Châteauguay\", \"Chambly\",\n \"Sainte-Julie\", \"Varennes\", \"Belœil\", \"McMasterville\", \"Mont-Saint-Hilaire\",\n \"Saint-Basile-le-Grand\", \"Saint-Bruno-de-Montarville\", \"Boisbriand\",\n \"Richelieu\", \"Beauharnois\", \"Salaberry-de-Valleyfield\", \"Vaudreuil-Dorion\",\n \"Kirkland\", \"Beaconsfield\", \"Dollard-des-Ormeaux\", \"Mont-Royal\", \"Outremont\",\n)\n\n\n@app.get(\"/api/stats\")\ndef stats(syncs_since_h: float | None = None):\n \"\"\"Stats globales + journal de synchronisation.\n\n `syncs_since_h` (optionnel) : fenêtre en heures pour `recent_syncs` — le\n moniteur central (api-ka) polle aux 2 h et les 20 dernières entrées ne\n couvrent que ~11 min d'activité sur ~200 sources (fausses alertes\n « stale »). Avec le paramètre : toutes les entrées des N dernières heures\n (max 4000) ; s'il y en a moins de 20, on retombe sur les 20 dernières\n toutes époques confondues. Sans le paramètre : comportement historique\n inchangé (LIMIT 20).\n \"\"\"\n con = db.connect()\n gm = \",\".join(\"?\" * len(_VILLES_GM))\n row = con.execute(\n f\"\"\"SELECT COUNT(*) total,\n SUM(CASE WHEN city='Québec' THEN 1 ELSE 0 END) quebec,\n SUM(CASE WHEN city='Lévis' THEN 1 ELSE 0 END) levis,\n SUM(CASE WHEN city IN ({gm}) THEN 1 ELSE 0 END) montreal,\n SUM(CASE WHEN city NOT IN ('Québec','Lévis') AND city NOT IN ({gm})\n THEN 1 ELSE 0 END) autres,\n COUNT(DISTINCT source) sources,\n AVG(price) avg_price\n FROM listings WHERE active=1 AND published=1 AND dup_of IS NULL\"\"\",\n list(_VILLES_GM) + list(_VILLES_GM)).fetchone()\n log: list[dict] = []\n if syncs_since_h is not None:\n import time as _time\n cutoff = _time.time() - syncs_since_h * 3600\n log = [dict(r) for r in con.execute(\n \"SELECT * FROM sync_log WHERE ts >= ? ORDER BY ts DESC LIMIT 4000\",\n (cutoff,))]\n if syncs_since_h is None or len(log) < 20:\n log = [dict(r) for r in con.execute(\n \"SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20\")]\n # qualité des données : complétude moyenne + quarantaine (quality.py)\n qual = con.execute(\n \"\"\"SELECT ROUND(AVG(completeness),1) avg_completeness,\n SUM(published=1) publiees, SUM(published=0) quarantaine\n FROM listings WHERE active=1\"\"\"\n ).fetchone()\n con.close()\n return {**dict(row), \"quality\": dict(qual), \"recent_syncs\": log}\n\n\n@app.get(\"/api/stats/detailed\")\ndef stats_detailed():\n \"\"\"Agrégats du marché (source unique : louka/marketstats.py).\"\"\"\n from . import marketstats\n return marketstats.compute()\n\n\n# --- Module Stats commun Groupe KA (contrat frontend/src/ka/stats/SPEC.md) ---\n_PERIODES_VALIDES = {\"auj\", \"7j\", \"30j\", \"3m\", \"6m\", \"12m\", \"annee\", \"tout\"}\n\n\n@app.get(\"/api/stats/dashboard\")\ndef stats_dashboard(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None):\n \"\"\"Tableau de bord analytique (KPI, séries, répartitions, tableaux…).\"\"\"\n from . import statsdash\n if period not in _PERIODES_VALIDES and not (from_ and to):\n period = \"30j\"\n return statsdash.compute(period, from_, to)\n\n\n@app.get(\"/api/stats/report\")\ndef stats_report(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None,\n mode: str = \"complet\"):\n \"\"\"Rapport statistique PDF estampillé Groupe-KA — 5 modes :\n complet | synthese | tendances | repartitions | donnees (inconnu → complet).\"\"\"\n from fastapi.responses import Response\n from . import kapdf, statsdash\n if period not in _PERIODES_VALIDES:\n period = \"perso\" if (from_ and to) else \"30j\"\n if mode not in kapdf.REPORT_MODES:\n mode = \"complet\"\n dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n from_, to)\n eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n data = kapdf.GroupeKAReport(site=site, dashboard=dash, mode=mode).build()\n fname = kapdf.filename(\"lou-ka\", period, mode)\n return Response(content=data, media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n f'attachment; filename=\"{fname}\"'})\n\n\n\n@app.get(\"/api/stats/catalog\")\ndef stats_catalog(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None):\n \"\"\"v3 — blocs composables pour le constructeur de rapports personnalisés.\"\"\"\n from . import kapdf, statsdash\n if period not in _PERIODES_VALIDES and not (from_ and to):\n period = \"30j\"\n dash = statsdash.compute(period if period in _PERIODES_VALIDES else \"30j\",\n from_, to)\n return {\"updated\": dash.get(\"updated\"), \"period\": dash.get(\"period\"),\n \"blocks\": kapdf.catalog(dash)}\n\n\n@app.post(\"/api/stats/report/custom\")\ndef stats_report_custom(spec: dict = Body(...)):\n \"\"\"v3 — rapport PDF personnalisé : {\"title\", \"period\", \"from\", \"to\",\n \"blocks\": [{\"key\": \"series:…\", \"render\": \"bar\"}, …]} (SPEC.md §3bis).\"\"\"\n from fastapi.responses import Response\n from . import kapdf, statsdash\n period = str(spec.get(\"period\") or \"30j\")\n from_ = str(spec.get(\"from\") or \"\") or None\n to = str(spec.get(\"to\") or \"\") or None\n if period not in _PERIODES_VALIDES:\n period = \"perso\" if (from_ and to) else \"30j\"\n dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n from_, to)\n known = {b[\"key\"] for b in kapdf.catalog(dash)}\n blocks = [b for b in (spec.get(\"blocks\") or [])\n if isinstance(b, dict) and b.get(\"key\") in known][:40]\n if not blocks:\n raise HTTPException(400, \"Aucun bloc valide dans la composition\")\n eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n data = kapdf.GroupeKAReport(\n site=site, dashboard=dash, mode=kapdf.CUSTOM_MODE,\n spec={\"title\": str(spec.get(\"title\") or \"\")[:80], \"blocks\": blocks},\n ).build()\n fname = kapdf.filename(\"lou-ka\", period, kapdf.CUSTOM_MODE)\n return Response(content=data, media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n f'attachment; filename=\"{fname}\"'})\n\n\n@app.post(\"/api/sync\")\ndef trigger_sync(background: BackgroundTasks, source: str | None = None):\n \"\"\"Déclenche une synchronisation (équivalent d'un webhook entrant).\"\"\"\n def _job():\n with _sync_lock:\n ingest.run([source] if source else None)\n background.add_task(_job)\n return {\"status\": \"démarré\", \"source\": source or \"toutes\"}\n\n\n# --- Frontend React (build Vite) --------------------------------------------\nif FRONTEND_DIST.exists():\n app.mount(\"/assets\", StaticFiles(directory=FRONTEND_DIST / \"assets\"), name=\"assets\")\n\n # référencement : SSR des routes publiques, robots.txt, sitemaps, 410.\n # Inclus AVANT le catch-all — l'ordre d'enregistrement fait foi.\n from . import seo # noqa: E402\n app.include_router(seo.router)\n\n # Documentation utilisateur (frontend/public/doc → dist/doc) — routes\n # explicites AVANT le catch-all SPA pour servir /doc/ comme un index.\n @app.get(\"/doc\")\n def doc_redirect():\n return RedirectResponse(\"/doc/\", status_code=308)\n\n @app.get(\"/doc/\")\n def doc_index():\n return FileResponse(FRONTEND_DIST / \"doc\" / \"index.html\")\n\n @app.middleware(\"http\")\n async def _cache_headers(request, call_next):\n \"\"\"Politique de cache : les bundles hachés (/assets/…) sont immuables,\n mais index.html doit TOUJOURS être revalidé — sinon les navigateurs\n gardent une vieille version de l'app après un déploiement.\"\"\"\n resp = await call_next(request)\n path = request.url.path\n if path.startswith(\"/assets/\"):\n resp.headers[\"Cache-Control\"] = \"public, max-age=31536000, immutable\"\n elif \"text/html\" in (resp.headers.get(\"content-type\") or \"\"):\n resp.headers[\"Cache-Control\"] = \"no-cache\"\n return resp\n\n # routes de l'app rendues uniquement côté client (privées ou volatiles) —\n # tout autre chemin inconnu renvoie index.html avec un statut 404 : le\n # routeur React affiche sa page « introuvable », les moteurs de recherche\n # voient un vrai 404 (fin des soft-404).\n _CLIENT_ROUTES = {\"profil\", \"favoris\", \"gestion\", \"bienvenue\", \"bot\",\n \"contact\", \"juste-valeur\", \"court-terme\"}\n _CLIENT_PREFIXES = (\"u/\", \"passerelle/\", \"court-terme/\")\n\n @app.get(\"/{full_path:path}\")\n def spa(full_path: str):\n target = FRONTEND_DIST / full_path\n if full_path and target.is_file():\n return FileResponse(target)\n known = (full_path in _CLIENT_ROUTES\n or any(full_path == p.rstrip(\"/\") or full_path.startswith(p)\n for p in _CLIENT_PREFIXES))\n return FileResponse(FRONTEND_DIST / \"index.html\",\n status_code=200 if known else 404,\n headers={\"Cache-Control\": \"no-cache\"})\n","numLines":932,"startLine":1,"totalLines":932}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"e035459f-4e27-4ffa-bbd1-b56b87fcbcbb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJyxbBrB7Zk35hUQuqn","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISxQIKpwEIERgCKkAIQvo5Wxvr83dWp2F8B/djwo3ReBijR10+RF/rQJyBMz7z26zM1NIeJMOe4TQEdMtp2LG9uVGW0mDrgTTvCU+PMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHe+LLUBrABAhIMg920c1g4zAHWyksEGgxQUjHLrXfJtwhzwRIiMOb4KJQosGccMj1SELQS7upvFjffK4arzaJs8MkDSfAcloXo8GfVjWNSs4DkRlQKoipLmfOptD7vOK9HTSEUpkfsMikPK4ze95FwAwxVwzap9ImuPdy5Z8nq6BCTco+k520YoU+YEswgbkfraEyprjn2JxmdV+a0cNIVROs+GAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":19026,"cache_read_input_tokens":86660,"cache_creation":{"ephemeral_5m_input_tokens":19026,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"03e747e3-be77-4805-9dfe-30ce658ebcbd"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJyxbBrB7Zk35hUQuqn","type":"message","role":"assistant","content":[{"type":"text","text":"Je lis maintenant la page fiche côté frontend pour comprendre les conventions UI."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":19026,"cache_read_input_tokens":86660,"cache_creation":{"ephemeral_5m_input_tokens":19026,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"02d98469-c25b-4ef1-b9ba-04346446ec60"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJyxbBrB7Zk35hUQuqn","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FvVSiKiNAdpPG4gtAgYRiK","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":19026,"cache_read_input_tokens":86660,"cache_creation":{"ephemeral_5m_input_tokens":19026,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1335f722-81de-434d-99bf-009c601d352d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FvVSiKiNAdpPG4gtAgYRiK","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n5\t// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n6\t// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n7\t// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n8\t// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n9\t// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n10\t// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n11\t// blocs sans `order` passaient devant la galerie sur mobile).\n12\t// -----------------------------------------------------------------------------\n13\timport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\n14\timport { Link, useParams } from \"react-router-dom\";\n15\timport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\n16\timport QuartierBlock from \"../components/QuartierBlock\";\n17\timport SmartImg from \"../components/SmartImg\";\n18\timport FairValueBadge from \"../components/FairValueBadge\";\n19\timport PriceAnalysis from \"../components/PriceAnalysis\";\n20\timport HistoriqueTAL from \"../components/HistoriqueTAL\";\n21\timport RegistreLoyers from \"../components/RegistreLoyers\";\n22\timport RisqueInondation from \"../components/RisqueInondation\";\n23\timport QualiteAir from \"../components/QualiteAir\";\n24\timport EssenceProche from \"../components/EssenceProche\";\n25\timport HydroEstimation from \"../components/HydroEstimation\";\n26\timport CommercesProches from \"../components/CommercesProches\";\n27\timport { IcoAlert, IcoDoc } from \"../components/Icons\";\n28\timport KaScoresBlock from \"../components/KaScoresBlock\";\n29\timport { markSeen } from \"../search/seen\";\n30\t\n31\t// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\n32\tconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n33\t\n34\t// Icônes et libellés des commodités de proximité (louka/poi.py)\n35\tconst POI_META: Record = {\n36\t epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n37\t depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n38\t pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n39\t ecole: { icon: \"🏫\", label: \"École\" },\n40\t garderie: { icon: \"🧸\", label: \"Garderie\" },\n41\t parc: { icon: \"🌳\", label: \"Parc\" },\n42\t bus: { icon: \"🚌\", label: \"Bus\" },\n43\t metro: { icon: \"🚇\", label: \"Métro\" },\n44\t gym: { icon: \"🏋️\", label: \"Gym\" },\n45\t cafe: { icon: \"☕\", label: \"Café\" },\n46\t clinique: { icon: \"🩺\", label: \"Clinique\" },\n47\t hopital: { icon: \"🏥\", label: \"Hôpital\" },\n48\t bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n49\t};\n50\t\n51\t// Regroupement des POI en catégories repliables\n52\tconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n53\t { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n54\t { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n55\t { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n56\t { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n57\t { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n58\t];\n59\t\n60\t// Badge « prix vs marché » — seuils configurables\n61\tconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n62\t\n63\tconst PETS_LABEL: Record = {\n64\t oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n65\t};\n66\t\n67\tconst NBSP = \" \";\n68\t\n69\t/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\n70\tconst fmtMarche = (m: number): string =>\n71\t `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n72\t\n73\tfunction badgeMarche(price: number | null | undefined,\n74\t loyerSecteur: number | null | undefined) {\n75\t if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n76\t const delta = (price - loyerSecteur) / loyerSecteur;\n77\t const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n78\t if (delta <= SEUILS_MARCHE.bonDeal)\n79\t return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n80\t if (delta <= SEUILS_MARCHE.dansLeMarche)\n81\t return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n82\t return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n83\t}\n84\t\n85\t/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\n86\tfunction badgesConfirmes(l: Listing): string[] {\n87\t const d = l.details ?? {};\n88\t const out: string[] = [];\n89\t const inc = d.inclusions ?? {};\n90\t if (inc.heating) out.push(\"Chauffage inclus\");\n91\t if (inc.electricity) out.push(\"Électricité incluse\");\n92\t if (inc.hot_water) out.push(\"Eau chaude incluse\");\n93\t if (inc.internet) out.push(\"Internet inclus\");\n94\t const app = d.appliances ?? {};\n95\t if (app.dishwasher) out.push(\"Lave-vaisselle\");\n96\t if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n97\t if (app.fridge && app.stove) out.push(\"Électroménagers\");\n98\t if (d.ac) out.push(\"Climatisation\");\n99\t if (d.elevator) out.push(\"Ascenseur\");\n100\t if (d.balcony) out.push(\"Balcon\");\n101\t if (d.pool) out.push(\"Piscine\");\n102\t if (d.gym) out.push(\"Gym\");\n103\t if (d.laundry) out.push(\"Buanderie\");\n104\t if (d.storage) out.push(\"Rangement\");\n105\t if (d.parking?.available)\n106\t out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n107\t if (l.furnished) out.push(\"Meublé\");\n108\t if (d.smoking === false) out.push(\"Non-fumeur\");\n109\t return out;\n110\t}\n111\t\n112\t// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\n113\tfunction Lightbox({ images, start, titre, onClose }:\n114\t { images: string[]; start: number; titre: string; onClose: () => void }) {\n115\t const [idx, setIdx] = useState(start);\n116\t const [scale, setScale] = useState(1);\n117\t const [tx, setTx] = useState(0);\n118\t const [ty, setTy] = useState(0);\n119\t const track = useRef(null);\n120\t const pointers = useRef(new Map());\n121\t const pinch = useRef<{ d: number; scale: number } | null>(null);\n122\t const lastTap = useRef(0);\n123\t\n124\t useEffect(() => { // position initiale + verrou du défilement de la page\n125\t track.current?.scrollTo({ left: start * track.current.clientWidth });\n126\t document.body.style.overflow = \"hidden\";\n127\t const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n128\t window.addEventListener(\"keydown\", onKey);\n129\t return () => {\n130\t document.body.style.overflow = \"\";\n131\t window.removeEventListener(\"keydown\", onKey);\n132\t };\n133\t // eslint-disable-next-line react-hooks/exhaustive-deps\n134\t }, []);\n135\t\n136\t const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n137\t const onScroll = () => {\n138\t const el = track.current;\n139\t if (el && scale === 1) {\n140\t const i = Math.round(el.scrollLeft / el.clientWidth);\n141\t if (i !== idx) { setIdx(i); resetZoom(); }\n142\t }\n143\t };\n144\t const dist = () => {\n145\t const [a, b] = [...pointers.current.values()];\n146\t return Math.hypot(a.x - b.x, a.y - b.y);\n147\t };\n148\t const onPointerDown = (e: React.PointerEvent) => {\n149\t pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n150\t if (pointers.current.size === 2)\n151\t pinch.current = { d: dist(), scale };\n152\t if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n153\t const now = Date.now();\n154\t if (now - lastTap.current < 300) {\n155\t if (scale > 1) resetZoom(); else setScale(2.5);\n156\t }\n157\t lastTap.current = now;\n158\t }\n159\t };\n160\t const onPointerMove = (e: React.PointerEvent) => {\n161\t const prev = pointers.current.get(e.pointerId);\n162\t if (!prev) return;\n163\t pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n164\t if (pointers.current.size === 2 && pinch.current) {\n165\t const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n166\t setScale(s);\n167\t if (s === 1) { setTx(0); setTy(0); }\n168\t } else if (pointers.current.size === 1 && scale > 1) {\n169\t setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n170\t setTy((v) => v + (e.clientY - prev.y));\n171\t }\n172\t };\n173\t const onPointerUp = (e: React.PointerEvent) => {\n174\t pointers.current.delete(e.pointerId);\n175\t if (pointers.current.size < 2) pinch.current = null;\n176\t };\n177\t\n178\t return (\n179\t
\n181\t \n182\t \n183\t {idx + 1}/{images.length}\n184\t \n185\t 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n188\t onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n189\t onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n190\t >\n191\t {images.map((u, i) => (\n192\t
\n193\t 1\n197\t ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n198\t : undefined}\n199\t />\n200\t
\n201\t ))}\n202\t
\n203\t {scale === 1 && idx > 0 && (\n204\t \n207\t )}\n208\t {scale === 1 && idx < images.length - 1 && (\n209\t \n212\t )}\n213\t \n214\t );\n215\t}\n216\t\n217\t// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\n218\tfunction Galerie({ images, titre, unitType }:\n219\t { images: string[]; titre: string; unitType?: string }) {\n220\t const [idx, setIdx] = useState(0);\n221\t const [zoom, setZoom] = useState(false);\n222\t const track = useRef(null);\n223\t\n224\t const onScroll = () => {\n225\t const el = track.current;\n226\t if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n227\t };\n228\t const goto = (i: number) =>\n229\t track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n230\t\n231\t if (images.length === 0)\n232\t return (\n233\t
\n234\t \n236\t
\n237\t );\n238\t\n239\t return (\n240\t <>\n241\t
\n242\t
\n243\t {images.map((u, i) => (\n244\t setZoom(true)}\n249\t />\n250\t ))}\n251\t
\n252\t {idx + 1}/{images.length}\n253\t {idx > 0 && (\n254\t \n255\t )}\n256\t {idx < images.length - 1 && (\n257\t \n258\t )}\n259\t
\n260\t {images.length > 1 && (\n261\t
\n262\t {images.map((u, i) => (\n263\t \n267\t ))}\n268\t
\n269\t )}\n270\t {zoom && (\n271\t setZoom(false)} />\n273\t )}\n274\t \n275\t );\n276\t}\n277\t\n278\texport default function ListingPage() {\n279\t const { uid } = useParams<{ uid: string }>();\n280\t const [l, setL] = useState(null);\n281\t const [error, setError] = useState(null);\n282\t\n283\t useEffect(() => {\n284\t fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n285\t if (!uid) return;\n286\t fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n287\t markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n288\t window.scrollTo(0, 0);\n289\t }, [uid]);\n290\t\n291\t if (error)\n292\t return (\n293\t
\n294\t
\n295\t

Annonce introuvable

\n296\t

{error}

\n297\t Retour aux logements\n298\t
\n299\t );\n300\t\n301\t if (!l)\n302\t return (\n303\t
\n304\t
\n305\t
\n306\t
\n307\t
\n308\t
\n309\t );\n310\t\n311\t const dg = l.digest ?? null;\n312\t const f = dg?.faits;\n313\t const conf = dg?.confiance ?? {};\n314\t const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n315\t const confirmes = badgesConfirmes(l);\n316\t const autres = l.amenities.filter(\n317\t (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n318\t const inc = l.details?.inclusions ?? {};\n319\t const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n320\t\n321\t const enLigneDepuis = l.first_seen\n322\t ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n323\t const hist = (l.price_history ?? []).filter((h) => h.price != null);\n324\t const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n325\t ? { de: hist[1].price!, a: hist[0].price! } : null;\n326\t\n327\t const updated = l.updated_at\n328\t ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n329\t day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n330\t\n331\t // chips clés (haute confiance seulement pour les faits extraits du texte)\n332\t const chips: string[] = [];\n333\t if (l.unit_type) chips.push(l.unit_type);\n334\t const dispo = fmtAvailability(l.availability_date);\n335\t if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n336\t if (l.furnished) chips.push(\"Meublé\");\n337\t if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n338\t if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n339\t if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n340\t chips.push(`${f.nb_occupants_total} occupants`);\n341\t if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n342\t chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n343\t if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n344\t\n345\t const pois = l.poi ?? [];\n346\t\n347\t return (\n348\t
\n349\t \n354\t\n355\t
\n356\t {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n357\t ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n358\t
\n359\t
\n360\t \n362\t
\n363\t\n364\t
\n365\t
\n366\t {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n367\t
\n368\t {l.fv_verdict\n369\t ?
\n370\t : deal &&
{deal.txt}
}\n371\t

{l.title || l.address}

\n372\t
\n373\t {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n374\t
\n375\t
\n376\t {chips.map((c) => {c})}\n377\t
\n378\t \n386\t \n388\t Voir l'annonce chez {sourceName(l.source)} ↗\n389\t \n390\t \n392\t Télécharger la fiche (PDF)\n393\t \n394\t
\n395\t\n396\t
\n397\t

Description

\n398\t {dg ? (\n399\t <>\n400\t {dg.en_bref &&

{dg.en_bref}

}\n401\t {dg.sections.map((s) => (\n402\t
\n403\t

{s.titre}

\n404\t

{s.texte}

\n405\t
\n406\t ))}\n407\t
\n408\t Voir le texte original de la source\n409\t

{l.description}

\n410\t
\n411\t \n412\t ) : (\n413\t l.description\n414\t ?

{l.description}

\n415\t :

La source ne fournit pas de description pour cette annonce.

\n416\t )}\n417\t
\n418\t\n419\t
\n420\t

Inclusions et commodités

\n421\t {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n422\t
\n423\t {confirmes.map((b) => (\n424\t ✓ {b}\n425\t ))}\n426\t {autres.map((a) => (\n427\t {a}\n428\t ))}\n429\t
\n430\t {confirmes.length === 0 && autres.length === 0 && (\n431\t

La source ne précise pas les inclusions.

\n432\t )}\n433\t
\n434\t\n435\t
\n436\t

Détails pratiques

\n437\t
\n438\t
Gestionnaire
{sourceName(l.source)}
\n439\t {l.price_label && (\n440\t
Prix affiché
{l.price_label}
\n441\t )}\n442\t {f?.duree_bail_minimale_mois && (\n443\t
Bail minimum
{f.duree_bail_minimale_mois} mois
\n444\t )}\n445\t {enLigneDepuis != null && (\n446\t
En ligne depuis
\n447\t
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n448\t )}\n449\t {updated && (\n450\t
Synchronisé
{updated}
\n451\t )}\n452\t
\n453\t {baissePrix && (\n454\t
\n455\t {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n456\t {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n457\t {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n458\t
\n459\t )}\n460\t
\n461\t
\n462\t\n463\t {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n464\t ------- KA Scores, quartier, proximité ---------------------------- */}\n465\t
\n466\t \n467\t\n468\t \n469\t\n470\t {l.lat != null && l.lng != null && (\n471\t
\n472\t

Emplacement

\n473\t }>\n474\t \n475\t \n476\t

\n477\t Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n478\t Position selon l'adresse géocodée (Adresses Québec).\n479\t

\n480\t
\n481\t )}\n482\t\n483\t \n484\t\n485\t \n486\t\n487\t \n488\t\n489\t \n490\t\n491\t \n492\t\n493\t \n494\t\n495\t {l.kascores && }\n496\t\n497\t
\n498\t {l.quartier ? : null}\n499\t
\n500\t\n501\t
\n502\t {pois.length > 0 && (\n503\t <>\n504\t

À proximité

\n505\t {POI_GROUPES.map((g, gi) => {\n506\t const items = pois.filter((p) => g.cats.includes(p.cat));\n507\t if (items.length === 0) return null;\n508\t return (\n509\t
\n510\t \n511\t {g.icone} {g.titre}\n512\t \n513\t {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n514\t \n515\t \n516\t
    \n517\t {items.map((p) => {\n518\t const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n519\t return (\n520\t
  • \n521\t {meta.icon}\n522\t {p.name}\n523\t {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n524\t
  • \n525\t );\n526\t })}\n527\t
\n528\t
\n529\t );\n530\t })}\n531\t
\n532\t Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n533\t
\n534\t \n535\t )}\n536\t
\n537\t
\n538\t
\n539\t\n540\t
\n541\t {updated && <>Dernière synchronisation : {updated}. }\n542\t Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n543\t renvoie à l'annonce originale.\n544\t
\n545\t\n546\t {/* CTA sticky mobile — toujours visible */}\n547\t
\n548\t {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n549\t \n551\t Voir chez {sourceName(l.source)} ↗\n552\t \n553\t
\n554\t
\n555\t );\n556\t}\n557\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c8a3a833-7c64-416e-a4bc-4419ef5c0f0a","timestamp":"2026-08-24T21:49:19.268Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n// blocs sans `order` passaient devant la galerie sur mobile).\n// -----------------------------------------------------------------------------\nimport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\nimport QuartierBlock from \"../components/QuartierBlock\";\nimport SmartImg from \"../components/SmartImg\";\nimport FairValueBadge from \"../components/FairValueBadge\";\nimport PriceAnalysis from \"../components/PriceAnalysis\";\nimport HistoriqueTAL from \"../components/HistoriqueTAL\";\nimport RegistreLoyers from \"../components/RegistreLoyers\";\nimport RisqueInondation from \"../components/RisqueInondation\";\nimport QualiteAir from \"../components/QualiteAir\";\nimport EssenceProche from \"../components/EssenceProche\";\nimport HydroEstimation from \"../components/HydroEstimation\";\nimport CommercesProches from \"../components/CommercesProches\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";\nimport { markSeen } from \"../search/seen\";\n\n// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\nconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n\n// Icônes et libellés des commodités de proximité (louka/poi.py)\nconst POI_META: Record = {\n epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n ecole: { icon: \"🏫\", label: \"École\" },\n garderie: { icon: \"🧸\", label: \"Garderie\" },\n parc: { icon: \"🌳\", label: \"Parc\" },\n bus: { icon: \"🚌\", label: \"Bus\" },\n metro: { icon: \"🚇\", label: \"Métro\" },\n gym: { icon: \"🏋️\", label: \"Gym\" },\n cafe: { icon: \"☕\", label: \"Café\" },\n clinique: { icon: \"🩺\", label: \"Clinique\" },\n hopital: { icon: \"🏥\", label: \"Hôpital\" },\n bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n};\n\n// Regroupement des POI en catégories repliables\nconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n];\n\n// Badge « prix vs marché » — seuils configurables\nconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n\nconst PETS_LABEL: Record = {\n oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n};\n\nconst NBSP = \" \";\n\n/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\nconst fmtMarche = (m: number): string =>\n `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n\nfunction badgeMarche(price: number | null | undefined,\n loyerSecteur: number | null | undefined) {\n if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n const delta = (price - loyerSecteur) / loyerSecteur;\n const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n if (delta <= SEUILS_MARCHE.bonDeal)\n return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n if (delta <= SEUILS_MARCHE.dansLeMarche)\n return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n}\n\n/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\nfunction badgesConfirmes(l: Listing): string[] {\n const d = l.details ?? {};\n const out: string[] = [];\n const inc = d.inclusions ?? {};\n if (inc.heating) out.push(\"Chauffage inclus\");\n if (inc.electricity) out.push(\"Électricité incluse\");\n if (inc.hot_water) out.push(\"Eau chaude incluse\");\n if (inc.internet) out.push(\"Internet inclus\");\n const app = d.appliances ?? {};\n if (app.dishwasher) out.push(\"Lave-vaisselle\");\n if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n if (app.fridge && app.stove) out.push(\"Électroménagers\");\n if (d.ac) out.push(\"Climatisation\");\n if (d.elevator) out.push(\"Ascenseur\");\n if (d.balcony) out.push(\"Balcon\");\n if (d.pool) out.push(\"Piscine\");\n if (d.gym) out.push(\"Gym\");\n if (d.laundry) out.push(\"Buanderie\");\n if (d.storage) out.push(\"Rangement\");\n if (d.parking?.available)\n out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n if (l.furnished) out.push(\"Meublé\");\n if (d.smoking === false) out.push(\"Non-fumeur\");\n return out;\n}\n\n// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\nfunction Lightbox({ images, start, titre, onClose }:\n { images: string[]; start: number; titre: string; onClose: () => void }) {\n const [idx, setIdx] = useState(start);\n const [scale, setScale] = useState(1);\n const [tx, setTx] = useState(0);\n const [ty, setTy] = useState(0);\n const track = useRef(null);\n const pointers = useRef(new Map());\n const pinch = useRef<{ d: number; scale: number } | null>(null);\n const lastTap = useRef(0);\n\n useEffect(() => { // position initiale + verrou du défilement de la page\n track.current?.scrollTo({ left: start * track.current.clientWidth });\n document.body.style.overflow = \"hidden\";\n const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n window.addEventListener(\"keydown\", onKey);\n return () => {\n document.body.style.overflow = \"\";\n window.removeEventListener(\"keydown\", onKey);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n const onScroll = () => {\n const el = track.current;\n if (el && scale === 1) {\n const i = Math.round(el.scrollLeft / el.clientWidth);\n if (i !== idx) { setIdx(i); resetZoom(); }\n }\n };\n const dist = () => {\n const [a, b] = [...pointers.current.values()];\n return Math.hypot(a.x - b.x, a.y - b.y);\n };\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2)\n pinch.current = { d: dist(), scale };\n if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n const now = Date.now();\n if (now - lastTap.current < 300) {\n if (scale > 1) resetZoom(); else setScale(2.5);\n }\n lastTap.current = now;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2 && pinch.current) {\n const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n setScale(s);\n if (s === 1) { setTx(0); setTy(0); }\n } else if (pointers.current.size === 1 && scale > 1) {\n setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n setTy((v) => v + (e.clientY - prev.y));\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size < 2) pinch.current = null;\n };\n\n return (\n
\n \n \n {idx + 1}/{images.length}\n \n 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n >\n {images.map((u, i) => (\n
\n 1\n ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n : undefined}\n />\n
\n ))}\n
\n {scale === 1 && idx > 0 && (\n \n )}\n {scale === 1 && idx < images.length - 1 && (\n \n )}\n
\n );\n}\n\n// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\nfunction Galerie({ images, titre, unitType }:\n { images: string[]; titre: string; unitType?: string }) {\n const [idx, setIdx] = useState(0);\n const [zoom, setZoom] = useState(false);\n const track = useRef(null);\n\n const onScroll = () => {\n const el = track.current;\n if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n };\n const goto = (i: number) =>\n track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n\n if (images.length === 0)\n return (\n
\n \n
\n );\n\n return (\n <>\n
\n
\n {images.map((u, i) => (\n setZoom(true)}\n />\n ))}\n
\n {idx + 1}/{images.length}\n {idx > 0 && (\n \n )}\n {idx < images.length - 1 && (\n \n )}\n
\n {images.length > 1 && (\n
\n {images.map((u, i) => (\n \n ))}\n
\n )}\n {zoom && (\n setZoom(false)} />\n )}\n \n );\n}\n\nexport default function ListingPage() {\n const { uid } = useParams<{ uid: string }>();\n const [l, setL] = useState(null);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n if (!uid) return;\n fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n window.scrollTo(0, 0);\n }, [uid]);\n\n if (error)\n return (\n
\n
\n

Annonce introuvable

\n

{error}

\n Retour aux logements\n
\n );\n\n if (!l)\n return (\n
\n
\n
\n
\n
\n
\n );\n\n const dg = l.digest ?? null;\n const f = dg?.faits;\n const conf = dg?.confiance ?? {};\n const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n const confirmes = badgesConfirmes(l);\n const autres = l.amenities.filter(\n (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n const inc = l.details?.inclusions ?? {};\n const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n\n const enLigneDepuis = l.first_seen\n ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n const hist = (l.price_history ?? []).filter((h) => h.price != null);\n const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n ? { de: hist[1].price!, a: hist[0].price! } : null;\n\n const updated = l.updated_at\n ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n\n // chips clés (haute confiance seulement pour les faits extraits du texte)\n const chips: string[] = [];\n if (l.unit_type) chips.push(l.unit_type);\n const dispo = fmtAvailability(l.availability_date);\n if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n if (l.furnished) chips.push(\"Meublé\");\n if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n chips.push(`${f.nb_occupants_total} occupants`);\n if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n\n const pois = l.poi ?? [];\n\n return (\n
\n \n\n
\n {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n
\n
\n \n
\n\n
\n
\n {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n
\n {l.fv_verdict\n ?
\n : deal &&
{deal.txt}
}\n

{l.title || l.address}

\n
\n {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n
\n
\n {chips.map((c) => {c})}\n
\n \n \n Voir l'annonce chez {sourceName(l.source)} ↗\n \n \n Télécharger la fiche (PDF)\n \n
\n\n
\n

Description

\n {dg ? (\n <>\n {dg.en_bref &&

{dg.en_bref}

}\n {dg.sections.map((s) => (\n
\n

{s.titre}

\n

{s.texte}

\n
\n ))}\n
\n Voir le texte original de la source\n

{l.description}

\n
\n \n ) : (\n l.description\n ?

{l.description}

\n :

La source ne fournit pas de description pour cette annonce.

\n )}\n
\n\n
\n

Inclusions et commodités

\n {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n
\n {confirmes.map((b) => (\n ✓ {b}\n ))}\n {autres.map((a) => (\n {a}\n ))}\n
\n {confirmes.length === 0 && autres.length === 0 && (\n

La source ne précise pas les inclusions.

\n )}\n
\n\n
\n

Détails pratiques

\n
\n
Gestionnaire
{sourceName(l.source)}
\n {l.price_label && (\n
Prix affiché
{l.price_label}
\n )}\n {f?.duree_bail_minimale_mois && (\n
Bail minimum
{f.duree_bail_minimale_mois} mois
\n )}\n {enLigneDepuis != null && (\n
En ligne depuis
\n
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n )}\n {updated && (\n
Synchronisé
{updated}
\n )}\n
\n {baissePrix && (\n
\n {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n
\n )}\n
\n
\n\n {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n ------- KA Scores, quartier, proximité ---------------------------- */}\n
\n \n\n \n\n {l.lat != null && l.lng != null && (\n
\n

Emplacement

\n }>\n \n \n

\n Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n Position selon l'adresse géocodée (Adresses Québec).\n

\n
\n )}\n\n \n\n \n\n \n\n \n\n \n\n \n\n {l.kascores && }\n\n
\n {l.quartier ? : null}\n
\n\n
\n {pois.length > 0 && (\n <>\n

À proximité

\n {POI_GROUPES.map((g, gi) => {\n const items = pois.filter((p) => g.cats.includes(p.cat));\n if (items.length === 0) return null;\n return (\n
\n \n {g.icone} {g.titre}\n \n {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n \n \n
    \n {items.map((p) => {\n const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n return (\n
  • \n {meta.icon}\n {p.name}\n {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n
  • \n );\n })}\n
\n
\n );\n })}\n
\n Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n
\n \n )}\n
\n
\n
\n\n
\n {updated && <>Dernière synchronisation : {updated}. }\n Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n renvoie à l'annonce originale.\n
\n\n {/* CTA sticky mobile — toujours visible */}\n
\n {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n \n Voir chez {sourceName(l.source)} ↗\n \n
\n
\n );\n}\n","numLines":557,"startLine":1,"totalLines":557}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"9b7792cc-f874-4544-a11f-e3b01e7f9b67","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJzJ3yF4ccicefuXWbs","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11399,"cache_read_input_tokens":105686,"cache_creation":{"ephemeral_5m_input_tokens":11399,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"99f86a51-6ad2-4cbc-a438-d0a6f147d5e0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJzJ3yF4ccicefuXWbs","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS4wMKqAEIERgCKkD+WqtyUfMOs7RhMWf24hB9qbbXW3BRU3kUsvbACG+EGP3iPB2Qj7uL1FU3NtoXTRIe9/192uCHxpllPeB7Q1PXMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagBjvmy1AawAQISDJXIj/76iBypYfDGrBoMXE2ZU5zvHRiL1IllIjBSy0n+ezwuZ4Vuh35dVlZ2aINlc29PRrrQfEnMNhIxvzYjJwmuwcN2Qf7YE1Nt7ZQq5wHsig+VQrHBEJm/SG4qdRP7y0aTxqSNVFci+BrMpk8Dknv1omdQTEiNWrPMpI6zI5WW+Ao5W1JX0FLRas4TTWiTX2G5A/mV3uMPlCLyfxKStBRXEHlavd/N32PKhKlS1tui/ye4+5x6TQZFM8BM602Lpy1Xbhdocr+FpcYaE89DmPmZWuMJyH8HbOa/QDlEOylBTPll2NkPnR3zCamDVyEecdiTVxwYtGyxPBCHGg7vIzEQ8g6O32UQfZ4Zo8f+BwjBE9Zc6YGLiZgn2c7a0rxZ7qAJlnvx5KwKuc6tYqMCG/mAKh2IXEoYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11399,"cache_read_input_tokens":105686,"cache_creation":{"ephemeral_5m_input_tokens":11399,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ae8fc81f-d982-4bab-9a4c-75ccb20e1034"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJzJ3yF4ccicefuXWbs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SFLHs6bHW6tqqi5xc8KiQe","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/run.py"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11399,"cache_read_input_tokens":105686,"cache_creation":{"ephemeral_5m_input_tokens":11399,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0a110a5d-1263-4b46-9681-cb1d438fe996"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SFLHs6bHW6tqqi5xc8KiQe","type":"tool_result","content":"1\t#!/usr/bin/env python3\n2\t# -----------------------------------------------------------------------------\n3\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n4\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n5\t# run.py : point d'entrée — `sync`, `watch`, `serve`\n6\t# -----------------------------------------------------------------------------\n7\t\"\"\"Utilisation :\n8\t python run.py sync [source ...] # synchronise les annonces\n9\t python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n10\t python run.py sync-ct [source ...] # synchronise la section court terme\n11\t python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n12\t python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n13\t python run.py record # enregistre les fixtures de test d'une source\n14\t python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n15\t python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n16\t python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n17\t python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n18\t\"\"\"\n19\tfrom __future__ import annotations\n20\t\n21\timport os\n22\timport sys\n23\tfrom pathlib import Path\n24\t\n25\t# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n26\t_env = Path(__file__).parent / \".env\"\n27\tif _env.exists():\n28\t for line in _env.read_text().splitlines():\n29\t line = line.strip()\n30\t if line and not line.startswith(\"#\") and \"=\" in line:\n31\t k, _, v = line.partition(\"=\")\n32\t os.environ.setdefault(k.strip(), v.strip())\n33\t\n34\t\n35\tdef main() -> None:\n36\t cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n37\t if cmd == \"sync\":\n38\t from louka import ingest\n39\t ingest.run(sys.argv[2:] or None)\n40\t elif cmd == \"watch\":\n41\t from louka import ingest\n42\t minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n43\t ingest.watch(minutes * 60)\n44\t elif cmd == \"sync-ct\":\n45\t from louka.shortterm import ingest as ct_ingest\n46\t ct_ingest.run(sys.argv[2:] or None)\n47\t elif cmd == \"watch-ct\":\n48\t from louka.shortterm import ingest as ct_ingest\n49\t minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n50\t ct_ingest.watch(minutes * 60)\n51\t elif cmd == \"geocode1\":\n52\t from louka import geocode\n53\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n54\t geocode.run(limit)\n55\t elif cmd == \"geocode\":\n56\t # lot Adresses Québec (rapide) ; « geocode1 » = ancien mode 1-par-1\n57\t from louka import geocode\n58\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n59\t geocode.run_batch(limit)\n60\t elif cmd == \"quality\":\n61\t # (re)calcule le score de complétude + publication/quarantaine\n62\t from louka import quality\n63\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n64\t quality.backfill(limit)\n65\t elif cmd == \"imgaudit\":\n66\t # contrôle qualité des images (liens morts, minuscules, placeholders,\n67\t # doublons) — incrémental : python run.py imgaudit [n_annonces] [source]\n68\t from louka import imgcheck\n69\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n70\t src = sys.argv[3] if len(sys.argv) > 3 else None\n71\t imgcheck.run(limit, source=src)\n72\t elif cmd == \"fairvalue\":\n73\t # (re)calcule la juste valeur locative de toutes les annonces publiées\n74\t from louka import fairvalue\n75\t fairvalue.compute_all()\n76\t elif cmd == \"poi\":\n77\t from louka import poi\n78\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n79\t poi.run(limit)\n80\t elif cmd == \"quartier\":\n81\t from louka import quartier\n82\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n83\t quartier.enrich(limit)\n84\t elif cmd == \"env\":\n85\t # données d'environnement OSM par tuile (routes, rails, cyclable,\n86\t # bars, POI complets) — cache ~3 mois, socle des KA Scores\n87\t from louka import environment\n88\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n89\t print(environment.run(limit))\n90\t elif cmd == \"kascores\":\n91\t # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n92\t # incrémental ; « python run.py kascores all » force tout le parc\n93\t from louka import kascores\n94\t force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n95\t print(kascores.run(recompute_all=force))\n96\t elif cmd == \"inondation-build\":\n97\t from louka import inondation\n98\t inondation.build()\n99\t elif cmd == \"air-refresh\":\n100\t from louka import air\n101\t air.refresh()\n102\t elif cmd == \"gaz-refresh\":\n103\t from louka import gaz\n104\t print(f\"[gaz] {gaz.refresh()} stations\")\n105\t elif cmd == \"hydro-precompute\":\n106\t from louka import hydro\n107\t n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n108\t b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n109\t hydro.precompute(limit=n, budget=b)\n110\t elif cmd == \"rdl\":\n111\t from louka import rdl\n112\t rdl.refresh()\n113\t elif cmd == \"tal\":\n114\t # historique TAL (décisions SOQUIJ) par adresse, par lots\n115\t from louka import tal\n116\t n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n117\t b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n118\t tal.precompute(limit=n, budget=b)\n119\t elif cmd == \"record\":\n120\t from louka import fixtures\n121\t from louka.connectors import CONNECTORS\n122\t targets = sys.argv[2:] or sorted(CONNECTORS)\n123\t for sid in targets:\n124\t try:\n125\t print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n126\t except Exception as exc:\n127\t print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n128\t elif cmd == \"serve\":\n129\t import uvicorn\n130\t port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n131\t uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n132\t else:\n133\t print(__doc__)\n134\t sys.exit(1)\n135\t\n136\t\n137\tif __name__ == \"__main__\":\n138\t main()\n139\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"01a146c7-fefa-411a-b61b-f82d2f9f2f7e","timestamp":"2026-08-24T21:50:06.769Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/run.py","content":"#!/usr/bin/env python3\n# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# run.py : point d'entrée — `sync`, `watch`, `serve`\n# -----------------------------------------------------------------------------\n\"\"\"Utilisation :\n python run.py sync [source ...] # synchronise les annonces\n python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n python run.py sync-ct [source ...] # synchronise la section court terme\n python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n python run.py record # enregistre les fixtures de test d'une source\n python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom pathlib import Path\n\n# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n_env = Path(__file__).parent / \".env\"\nif _env.exists():\n for line in _env.read_text().splitlines():\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n k, _, v = line.partition(\"=\")\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef main() -> None:\n cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n if cmd == \"sync\":\n from louka import ingest\n ingest.run(sys.argv[2:] or None)\n elif cmd == \"watch\":\n from louka import ingest\n minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n ingest.watch(minutes * 60)\n elif cmd == \"sync-ct\":\n from louka.shortterm import ingest as ct_ingest\n ct_ingest.run(sys.argv[2:] or None)\n elif cmd == \"watch-ct\":\n from louka.shortterm import ingest as ct_ingest\n minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n ct_ingest.watch(minutes * 60)\n elif cmd == \"geocode1\":\n from louka import geocode\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n geocode.run(limit)\n elif cmd == \"geocode\":\n # lot Adresses Québec (rapide) ; « geocode1 » = ancien mode 1-par-1\n from louka import geocode\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n geocode.run_batch(limit)\n elif cmd == \"quality\":\n # (re)calcule le score de complétude + publication/quarantaine\n from louka import quality\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n quality.backfill(limit)\n elif cmd == \"imgaudit\":\n # contrôle qualité des images (liens morts, minuscules, placeholders,\n # doublons) — incrémental : python run.py imgaudit [n_annonces] [source]\n from louka import imgcheck\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n src = sys.argv[3] if len(sys.argv) > 3 else None\n imgcheck.run(limit, source=src)\n elif cmd == \"fairvalue\":\n # (re)calcule la juste valeur locative de toutes les annonces publiées\n from louka import fairvalue\n fairvalue.compute_all()\n elif cmd == \"poi\":\n from louka import poi\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n poi.run(limit)\n elif cmd == \"quartier\":\n from louka import quartier\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n quartier.enrich(limit)\n elif cmd == \"env\":\n # données d'environnement OSM par tuile (routes, rails, cyclable,\n # bars, POI complets) — cache ~3 mois, socle des KA Scores\n from louka import environment\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n print(environment.run(limit))\n elif cmd == \"kascores\":\n # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n # incrémental ; « python run.py kascores all » force tout le parc\n from louka import kascores\n force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n print(kascores.run(recompute_all=force))\n elif cmd == \"inondation-build\":\n from louka import inondation\n inondation.build()\n elif cmd == \"air-refresh\":\n from louka import air\n air.refresh()\n elif cmd == \"gaz-refresh\":\n from louka import gaz\n print(f\"[gaz] {gaz.refresh()} stations\")\n elif cmd == \"hydro-precompute\":\n from louka import hydro\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n hydro.precompute(limit=n, budget=b)\n elif cmd == \"rdl\":\n from louka import rdl\n rdl.refresh()\n elif cmd == \"tal\":\n # historique TAL (décisions SOQUIJ) par adresse, par lots\n from louka import tal\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n tal.precompute(limit=n, budget=b)\n elif cmd == \"record\":\n from louka import fixtures\n from louka.connectors import CONNECTORS\n targets = sys.argv[2:] or sorted(CONNECTORS)\n for sid in targets:\n try:\n print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n except Exception as exc:\n print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n elif cmd == \"serve\":\n import uvicorn\n port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n else:\n print(__doc__)\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","numLines":139,"startLine":1,"totalLines":139}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNJzJ3yF4ccicefuXWbs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011nKVVybBSHoRNpwjtUX81e","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueTAL.tsx"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11399,"cache_read_input_tokens":105686,"cache_creation":{"ephemeral_5m_input_tokens":11399,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"34bd550d-e6ea-4402-9601-4de6345d1c38"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011nKVVybBSHoRNpwjtUX81e","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// components/HistoriqueTAL.tsx : bloc « Historique TAL » (fiche)\n5\t// Décisions du Tribunal administratif du logement (diffusées par SOQUIJ)\n6\t// rendues à l'adresse de l'annonce : le locataire potentiel voit si\n7\t// l'immeuble a un historique de litiges (non-paiement, résiliation,\n8\t// expulsion, reprise/éviction, insalubrité…) et qui était à l'initiative\n9\t// du recours (propriétaire ou locataire). Par respect de la vie privée,\n10\t// aucun nom de partie n'est affiché — seulement la référence neutre\n11\t// (ex. « 2026 QCTAL 21509 ») avec lien vers le texte intégral sur SOQUIJ.\n12\t// -----------------------------------------------------------------------------\n13\timport { useEffect, useState } from \"react\";\n14\timport { fetchTal, TalHistory } from \"../api\";\n15\t\n16\tconst fmtDate = (d: string | null): string => {\n17\t if (!d) return \"—\";\n18\t const dt = new Date(d + \"T00:00:00\");\n19\t return Number.isNaN(dt.getTime())\n20\t ? d\n21\t : dt.toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" });\n22\t};\n23\t\n24\texport default function HistoriqueTAL({ address, city }:\n25\t { address: string | null; city: string | null }) {\n26\t const [d, setD] = useState(null);\n27\t useEffect(() => {\n28\t setD(null);\n29\t if (!address) return;\n30\t fetchTal(address, city).then(setD).catch(() => setD(null));\n31\t }, [address, city]);\n32\t if (!address || !d || d.status === \"na\" || d.status === \"error\") return null;\n33\t\n34\t const decisions = d.decisions ?? [];\n35\t const alerte = (d.eviction ?? 0) > 0 || (d.contre_locataire ?? 0) > 0;\n36\t return (\n37\t
\n38\t

Historique au TAL

\n39\t {d.status === \"pending\" && (\n40\t

\n41\t Vérification en cours{\" \"}\n42\t Cette adresse est en file de vérification auprès des décisions\n43\t publiées du Tribunal administratif du logement.\n44\t

\n45\t )}\n46\t {d.status === \"ok\" && decisions.length === 0 && (\n47\t

\n48\t Aucune décision trouvée{\" \"}\n49\t Aucune décision publiée du Tribunal administratif du logement n'a\n50\t été repérée à cette adresse.\n51\t

\n52\t )}\n53\t {d.status === \"ok\" && decisions.length > 0 && (\n54\t <>\n55\t

\n56\t \n57\t {d.n} décision{(d.n ?? 0) > 1 ? \"s\" : \"\"} au TAL\n58\t {\" \"}\n59\t {d.contre_locataire\n60\t ? `dont ${d.contre_locataire} à l'initiative du propriétaire`\n61\t : \"aucune à l'initiative du propriétaire\"}\n62\t {d.last_date ? ` — la plus récente : ${fmtDate(d.last_date)}` : \"\"}.\n63\t

\n64\t
    \n65\t {decisions.slice(0, 8).map((dec) => (\n66\t
  • \n67\t
    \n68\t {fmtDate(dec.date)}\n69\t {dec.demandeur && (\n70\t \n71\t demande du {dec.demandeur}\n72\t \n73\t )}\n74\t {dec.verdict && (\n75\t demande {dec.verdict}\n76\t )}\n77\t
    \n78\t {dec.tags.length > 0 && (\n79\t
    \n80\t {dec.tags.map((t) => (\n81\t {t}\n82\t ))}\n83\t
    \n84\t )}\n85\t \n87\t {dec.citation || \"Texte intégral\"} — SOQUIJ\n88\t \n89\t
  • \n90\t ))}\n91\t
\n92\t \n93\t )}\n94\t

\n95\t Décisions publiques du{\" \"}\n96\t Tribunal administratif du logement,\n98\t diffusées par SOQUIJ (citoyens.soquij.qc.ca) et repérées par\n99\t correspondance d'adresse dans le texte des décisions — vérification\n100\t automatisée fournie à titre indicatif, sans valeur juridique. Une\n101\t décision peut concerner un autre logement du même immeuble.\n102\t

\n103\t
\n104\t );\n105\t}\n106\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fd437eb6-7058-4202-93e5-79cf68bfe87c","timestamp":"2026-08-24T21:50:07.422Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueTAL.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/HistoriqueTAL.tsx : bloc « Historique TAL » (fiche)\n// Décisions du Tribunal administratif du logement (diffusées par SOQUIJ)\n// rendues à l'adresse de l'annonce : le locataire potentiel voit si\n// l'immeuble a un historique de litiges (non-paiement, résiliation,\n// expulsion, reprise/éviction, insalubrité…) et qui était à l'initiative\n// du recours (propriétaire ou locataire). Par respect de la vie privée,\n// aucun nom de partie n'est affiché — seulement la référence neutre\n// (ex. « 2026 QCTAL 21509 ») avec lien vers le texte intégral sur SOQUIJ.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { fetchTal, TalHistory } from \"../api\";\n\nconst fmtDate = (d: string | null): string => {\n if (!d) return \"—\";\n const dt = new Date(d + \"T00:00:00\");\n return Number.isNaN(dt.getTime())\n ? d\n : dt.toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" });\n};\n\nexport default function HistoriqueTAL({ address, city }:\n { address: string | null; city: string | null }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n if (!address) return;\n fetchTal(address, city).then(setD).catch(() => setD(null));\n }, [address, city]);\n if (!address || !d || d.status === \"na\" || d.status === \"error\") return null;\n\n const decisions = d.decisions ?? [];\n const alerte = (d.eviction ?? 0) > 0 || (d.contre_locataire ?? 0) > 0;\n return (\n
\n

Historique au TAL

\n {d.status === \"pending\" && (\n

\n Vérification en cours{\" \"}\n Cette adresse est en file de vérification auprès des décisions\n publiées du Tribunal administratif du logement.\n

\n )}\n {d.status === \"ok\" && decisions.length === 0 && (\n

\n Aucune décision trouvée{\" \"}\n Aucune décision publiée du Tribunal administratif du logement n'a\n été repérée à cette adresse.\n

\n )}\n {d.status === \"ok\" && decisions.length > 0 && (\n <>\n

\n \n {d.n} décision{(d.n ?? 0) > 1 ? \"s\" : \"\"} au TAL\n {\" \"}\n {d.contre_locataire\n ? `dont ${d.contre_locataire} à l'initiative du propriétaire`\n : \"aucune à l'initiative du propriétaire\"}\n {d.last_date ? ` — la plus récente : ${fmtDate(d.last_date)}` : \"\"}.\n

\n
    \n {decisions.slice(0, 8).map((dec) => (\n
  • \n
    \n {fmtDate(dec.date)}\n {dec.demandeur && (\n \n demande du {dec.demandeur}\n \n )}\n {dec.verdict && (\n demande {dec.verdict}\n )}\n
    \n {dec.tags.length > 0 && (\n
    \n {dec.tags.map((t) => (\n {t}\n ))}\n
    \n )}\n \n {dec.citation || \"Texte intégral\"} — SOQUIJ\n \n
  • \n ))}\n
\n \n )}\n

\n Décisions publiques du{\" \"}\n Tribunal administratif du logement,\n diffusées par SOQUIJ (citoyens.soquij.qc.ca) et repérées par\n correspondance d'adresse dans le texte des décisions — vérification\n automatisée fournie à titre indicatif, sans valeur juridique. Une\n décision peut concerner un autre logement du même immeuble.\n

\n
\n );\n}\n","numLines":106,"startLine":1,"totalLines":106}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"004a95e4-e9bd-4883-a7f6-49c06eb4492b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNK3qoX94s2ZuFpXNXMQ","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7921,"cache_read_input_tokens":117085,"cache_creation":{"ephemeral_5m_input_tokens":7921,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bdf89f25-8ac6-4751-80ac-2f9c135f0192"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNK3qoX94s2ZuFpXNXMQ","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS5woKqAEIERgCKkCMmoLJOmiavZbMfRunUL+l1h7VoSYJTe/no6QlHsW1C3+N1jBNjcrl626rbVP0RiIJRZ1wHseFbtROYtJGnWZnMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagB4Pmy1AawAQISDDwEKukSQsQKVlx0/xoMHtgeBA3SJT7pCry9IjBurDKQPXB6HU6qQfPKsbSmR3DMJhedG2xWbtcPdC37o4cuxV2maA1mO2jSbADGlc8q6wgIc7Jo1C4FgZCcqPI5od1POiH1XwIno0vaS1aIzhIS7Mwzu7EEzbaSdUYhsEjgTQCcS9yJ7T05pVCn9KbRvMHRpuMrohFxhppfKAmyiMeQdsnseTifH/v76pzw0Nn5dbkOqJpfuROCf3gizcw5KlyGdRP2ACUJ1WQNVt+vBvORWLqqjTWVIf+ngTWwNBt56tpxbZ9RdDnqDwVGyCmpVmPjKGUZ8P+wBo4s+0IcWLb+9VR+l3LrfB/H1HhZAWbS1ruvsnnWFP40ETga4JMdkQvVsHJYZEqxsnMp9mKk7u+ZdEc89trMK9ysJYtOl6JvJrVoaTl8GWrffRxOGOy6kakGR2+q3fBig2mb/Rr8Cfotb5caFfq/gu/7phNhZceF+tE5G0rF0BXyoTrB05y2KCPPn46bBo2862vA8fmvgYRUbDTJIhWAxNBJgIhwHQ+APsMi3wH11X8LHhf/UhghQX9KP+vGHNYo97l4Y8evSdbR2us1kIqOLq2OZek6l2wcTaaDE1S94sJ27pv/86Zqv0Tmi8YdWFqIaJNv1ejONxHn/hQ2e5FuTAG/0hgSW1YHRSneJX1SYwREAuQyyS5Gilu1YgDP0zTlFNZYjm7toTkNtfR9UgzaG82C6XNs77N4tC0iONqiBIBqOwJydfisq2CwU87sOGNXuC9YiJvR4bCktTp11KFmE0I7z7/YpRkWH98E1a7DcWiTox/U9Qv+DHvCYZxf2GFnSu4qvrxJ0k/iiLXcm2G3FrPVj9zqeewLsyVF3rEYCLaGWMSg4DFHC7FuhZknurSDrwkoHefgs3T+Up7eNE8zEz0ku70z7slcaOO1N0TBhm38g1a3RooMoEFtdgs3ApgQmVNeziLsFk7+yaG9K+EojxsNeZF/tGhQP3rjMaXm1kH4pv8nCLgwGjJ/TRHfRpGWcRDbFl4CwuHZccyJStWCinzh2OGvccYJrRZTDuecvnk8S10Grqgn8zj4dIWI7IUvQPg7iWbnawUyvMTS68nNWZItCQQlqE2nJUqvSad8Tg9kK01FPNcDPGWKQcyBYg5Mc9fdW96mjmpy7LyzlHgbYWOm+YyjS225JBT+V0auUrycybCiFH96MJl6Q3kip4JDk7jgoGZvCawdQxomr5Hr8YaMjVqbKr97EXItsKiFeVXWGAJoqFd1E7GOG8uc1xa0xmB1ZDj24PQ2ZecSwENuNiHVVmLkk26DyJkNi3n06WuthZ7/7SuiML+Frr6p3hKADfhzPCBNbpEifqpyupnUGBZkbaboK0dDBRyYkq9tde3e5+N/RXk0yBj7Ozo2A66gPbkqP6/8kwmHz+cZFalRx1SJNiVvJjvP4X3qoVgAVa4n56EOWJ0OQg+FWzB7fntoFVn+uaVMDndMHv3GXyktJ4jTIHzO3m/ZawEHJvNt7iPuJluqunzZmtMA2aWbzL9aBzPFCj3QTIKaZyOvjqL3mu2K6XE93dnRbv58+kEjfBhCzawKy1qgqkZdutHdph1UMMFM+jIYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7921,"cache_read_input_tokens":117085,"cache_creation":{"ephemeral_5m_input_tokens":7921,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ec29426d-f802-4117-916a-6e4d4a51338b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNK3qoX94s2ZuFpXNXMQ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CrwyJLTquVFkXp652A8cgx","name":"ToolSearch","input":{"query":"select:TodoWrite","max_results":1},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7921,"cache_read_input_tokens":117085,"cache_creation":{"ephemeral_5m_input_tokens":7921,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"08715e5b-8e57-451e-9a17-47367da8e501"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01CrwyJLTquVFkXp652A8cgx","content":[{"type":"tool_reference","tool_name":"TodoWrite"}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"43fe7173-72b5-4ef5-bf65-2645d9fd9f85","timestamp":"2026-08-24T21:51:29.343Z","tool_use_result":{"matches":["TodoWrite"],"query":"select:TodoWrite","total_deferred_tools":17}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNK3qoX94s2ZuFpXNXMQ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MZuBXZEynGJ1Y5WauRceRv","name":"Bash","input":{"command":"cd ~/apps/lou-ka && sed -n '1,80p' louka/hydro.py | head -80","description":"Interface du module hydro (estimation électricité)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7921,"cache_read_input_tokens":117085,"cache_creation":{"ephemeral_5m_input_tokens":7921,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"560c1ba4-258f-4ec8-ab99-dc33881f36d0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MZuBXZEynGJ1Y5WauRceRv","type":"tool_result","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# hydro.py : estimation du coût d'électricité à une adresse — Hydro-Québec\n#\n# Reproduit le parcours PUBLIC de l'outil « Estimer les coûts d'électricité\n# à une résidence » (session.hydroquebec.com), rétro-conçu depuis son SPA :\n# 1. POST {clWebServicePublicApiURL}public/api/v3_0/lieu-consommation\n# body {noCivique, codePostal, recaptchaResponse} -> liste de lieux\n# (idConso, cleUnique, adresse) ;\n# 2. POST {drcvPublicApiURL}public/api/v3_0/consommation/estimation\n# body {cleUnique, idConso} -> coût annuel estimé.\n# Les deux étapes exigent un jeton reCAPTCHA v2 valide\n# (site key 6Lf08Q0UAAAAABCA7z47p2tMxa5_fY0wmn8DDPsu). Il est obtenu via un\n# solveur enfichable (2captcha / CapSolver) ET les requêtes sortent par le\n# proxy résidentiel canadien Bright Data (contourne le géoblocage / anti-bot).\n#\n# Configuration (.env) :\n# HQ_CAPTCHA_KEY clé du solveur (obligatoire pour l'appel live)\n# HQ_CAPTCHA_PROVIDER \"2captcha\" (défaut) | \"capsolver\"\n# BRIGHTDATA_* / OXYLABS_* : proxy résidentiel (réutilise la pile projet)\n#\n# Sans HQ_CAPTCHA_KEY : estimate() renvoie {\"disponible\": False, ...} et le\n# bloc fiche reste masqué — jamais d'estimation inventée.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nimport sqlite3\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"hydro.db\"\nTTL = 180 * 86400 # succès : la conso bouge lentement\nNEG_TTL = 30 * 86400 # échec (adresse inconnue) : re-tenter dans 30 j\n # — borne le coût captcha des cycles watch\n\nSITE_KEY = \"6Lf08Q0UAAAAABCA7z47p2tMxa5_fY0wmn8DDPsu\"\nPAGE_URL = (\"https://session.hydroquebec.com/portail/fr/web/clientele/\"\n \"estimation-consommation\")\nURL_LIEU = (\"https://services-cl.solutions.hydroquebec.com/wsapi/webpublic/\"\n \"public/api/v3_0/lieu-consommation\")\nURL_ESTIM = (\"https://services-cl.solutions.hydroquebec.com/conso/webpublic/\"\n \"public/api/v3_0/consommation/estimation\")\nUA = (\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \"\n \"(KHTML, like Gecko) Chrome/126.0 Safari/537.36\")\n\n\n# --- proxy résidentiel (Bright Data Web Unlocker en repli d'Oxylabs) ----------\ndef _opener() -> urllib.request.OpenerDirector:\n proxy = None\n if os.environ.get(\"OXYLABS_PROXY_USER\"):\n u = os.environ[\"OXYLABS_PROXY_USER\"]\n p = os.environ[\"OXYLABS_PROXY_PASS\"]\n host = os.environ.get(\"OXYLABS_PROXY\", \"pr.oxylabs.io:7777\")\n proxy = f\"http://{u}-cc-CA:{p}@{host}\"\n if proxy:\n h = urllib.request.ProxyHandler({\"http\": proxy, \"https\": proxy})\n return urllib.request.build_opener(h)\n return urllib.request.build_opener()\n\n\n# --- solveur reCAPTCHA v2 enfichable ------------------------------------------\ndef _solve_captcha() -> str | None:\n key = os.environ.get(\"HQ_CAPTCHA_KEY\")\n if not key:\n return None\n provider = os.environ.get(\"HQ_CAPTCHA_PROVIDER\", \"2captcha\").lower()\n if provider == \"capsolver\":\n return _capsolver(key)\n return _twocaptcha(key)\n\n\ndef _twocaptcha(key: str) -> str | None:\n r = urllib.request.urlopen(urllib.request.Request(\n \"https://2captcha.com/in.php\",","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f385b6d7-1fed-4f9c-990b-0fa321d97c7e","timestamp":"2026-08-24T21:51:30.215Z","tool_use_result":{"stdout":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# hydro.py : estimation du coût d'électricité à une adresse — Hydro-Québec\n#\n# Reproduit le parcours PUBLIC de l'outil « Estimer les coûts d'électricité\n# à une résidence » (session.hydroquebec.com), rétro-conçu depuis son SPA :\n# 1. POST {clWebServicePublicApiURL}public/api/v3_0/lieu-consommation\n# body {noCivique, codePostal, recaptchaResponse} -> liste de lieux\n# (idConso, cleUnique, adresse) ;\n# 2. POST {drcvPublicApiURL}public/api/v3_0/consommation/estimation\n# body {cleUnique, idConso} -> coût annuel estimé.\n# Les deux étapes exigent un jeton reCAPTCHA v2 valide\n# (site key 6Lf08Q0UAAAAABCA7z47p2tMxa5_fY0wmn8DDPsu). Il est obtenu via un\n# solveur enfichable (2captcha / CapSolver) ET les requêtes sortent par le\n# proxy résidentiel canadien Bright Data (contourne le géoblocage / anti-bot).\n#\n# Configuration (.env) :\n# HQ_CAPTCHA_KEY clé du solveur (obligatoire pour l'appel live)\n# HQ_CAPTCHA_PROVIDER \"2captcha\" (défaut) | \"capsolver\"\n# BRIGHTDATA_* / OXYLABS_* : proxy résidentiel (réutilise la pile projet)\n#\n# Sans HQ_CAPTCHA_KEY : estimate() renvoie {\"disponible\": False, ...} et le\n# bloc fiche reste masqué — jamais d'estimation inventée.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nimport sqlite3\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"hydro.db\"\nTTL = 180 * 86400 # succès : la conso bouge lentement\nNEG_TTL = 30 * 86400 # échec (adresse inconnue) : re-tenter dans 30 j\n # — borne le coût captcha des cycles watch\n\nSITE_KEY = \"6Lf08Q0UAAAAABCA7z47p2tMxa5_fY0wmn8DDPsu\"\nPAGE_URL = (\"https://session.hydroquebec.com/portail/fr/web/clientele/\"\n \"estimation-consommation\")\nURL_LIEU = (\"https://services-cl.solutions.hydroquebec.com/wsapi/webpublic/\"\n \"public/api/v3_0/lieu-consommation\")\nURL_ESTIM = (\"https://services-cl.solutions.hydroquebec.com/conso/webpublic/\"\n \"public/api/v3_0/consommation/estimation\")\nUA = (\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \"\n \"(KHTML, like Gecko) Chrome/126.0 Safari/537.36\")\n\n\n# --- proxy résidentiel (Bright Data Web Unlocker en repli d'Oxylabs) ----------\ndef _opener() -> urllib.request.OpenerDirector:\n proxy = None\n if os.environ.get(\"OXYLABS_PROXY_USER\"):\n u = os.environ[\"OXYLABS_PROXY_USER\"]\n p = os.environ[\"OXYLABS_PROXY_PASS\"]\n host = os.environ.get(\"OXYLABS_PROXY\", \"pr.oxylabs.io:7777\")\n proxy = f\"http://{u}-cc-CA:{p}@{host}\"\n if proxy:\n h = urllib.request.ProxyHandler({\"http\": proxy, \"https\": proxy})\n return urllib.request.build_opener(h)\n return urllib.request.build_opener()\n\n\n# --- solveur reCAPTCHA v2 enfichable ------------------------------------------\ndef _solve_captcha() -> str | None:\n key = os.environ.get(\"HQ_CAPTCHA_KEY\")\n if not key:\n return None\n provider = os.environ.get(\"HQ_CAPTCHA_PROVIDER\", \"2captcha\").lower()\n if provider == \"capsolver\":\n return _capsolver(key)\n return _twocaptcha(key)\n\n\ndef _twocaptcha(key: str) -> str | None:\n r = urllib.request.urlopen(urllib.request.Request(\n \"https://2captcha.com/in.php\",","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8b68461f-91ee-4821-949b-19114ac37238","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNK9wcvwhmm99kMzuQu8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ENCRHkJNS4Xa2k9W1dEe8w","name":"TodoWrite","input":{"todos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"in_progress","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"pending","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"pending","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":119402,"cache_read_input_tokens":16221,"cache_creation":{"ephemeral_5m_input_tokens":119402,"ephemeral_1h_input_tokens":0},"output_tokens":25,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a7db3613-3fd4-4e6b-90cb-c2473f77023d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ENCRHkJNS4Xa2k9W1dEe8w","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3841e17f-561e-4ab7-841b-6cbd5107368a","timestamp":"2026-08-24T21:51:42.865Z","tool_use_result":{"oldTodos":[],"newTodos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"in_progress","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"pending","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"pending","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c538828f-25af-43b6-9a97-268a2841949f","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKAsfq9gXkEmjuHB4FP","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS3AIKpwEIERgCKkC1ERruiA5S8tJSYa3bHNfBStMLMPOJCNsBVt/MGzs+txSJ+QjbLi8mhzWVFoxwpj5VBoR+NLDcXreGckYqVPQKMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH2+bLUBrABAhIMfd7gP2g/2/FKxFOPGgwUpB6gHlXTUrameGYiMO7GCKDEh4SF6zUz2gF5b/0HB6LsUqIIREZDBLaEpvEEpFnkDsBuMysm7wkKt8HiYypiXWNQQsFVB5P+YCEYx0GpJ5/BWRwoX8pDTyRlkg8HN28ROnTalzGJLycC9l/UHtY4W63PhQo8sMmgGAdCyKODR/fQFKvtvtS7jc3btGX7kIjKDqNEy+Ly/mXFz3M47n7Fbl0YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1079,"cache_read_input_tokens":135623,"cache_creation":{"ephemeral_5m_input_tokens":1079,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0a65257c-25cc-413a-b7ae-47847d7566fd"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKAsfq9gXkEmjuHB4FP","type":"message","role":"assistant","content":[{"type":"text","text":"Étape 1 — configuration SerpApi (clé server-side seulement, `.env` est déjà gitignoré)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1079,"cache_read_input_tokens":135623,"cache_creation":{"ephemeral_5m_input_tokens":1079,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"21178a47-af85-4870-984d-4674a7bdb925"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKAsfq9gXkEmjuHB4FP","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PAaqmLhnpXj4KCPmWTSfvJ","name":"Bash","input":{"command":"cd ~/apps/lou-ka && printf '\\n# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\\nSERPAPI_API_KEY=cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33\\n' >> .env && tail -3 .env","description":"Ajoute SERPAPI_API_KEY au .env local"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1079,"cache_read_input_tokens":135623,"cache_creation":{"ephemeral_5m_input_tokens":1079,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c46bbdba-fd45-42f2-823d-c6f5a763e207"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PAaqmLhnpXj4KCPmWTSfvJ","type":"tool_result","content":"# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\nSERPAPI_API_KEY=cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b84f7d18-fd14-4114-b1a1-55363f15b8ba","timestamp":"2026-08-24T21:51:53.390Z","tool_use_result":{"stdout":"# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\nSERPAPI_API_KEY=cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKAsfq9gXkEmjuHB4FP","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DJHVnVaTwafVh6E2c5DUMW","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/.env.example","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — variables d'environnement (modèle — copier vers .env, NE PAS COMMITTER .env)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# -----------------------------------------------------------------------------\n\n# Connecteurs / scraping\nFIRECRAWL_API_KEY=\nSCRAPFLY_KEY=\nAPIFY_TOKEN=\nOXYLABS_PROXY_USER=\nOXYLABS_PROXY_PASS=\nBRIGHTDATA_API_KEY=\nBRIGHTDATA_ZONE=\n\n# Session / auth\nSESSION_SECRET=\nLOUKA_BASE_URL=https://www.lou-ka.com\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nKA_SSO_SECRET=\nKA_HUB_URL=https://www.groupe-ka.com\n\n# Hydro-Québec (estimation électricité — solveur reCAPTCHA)\nHQ_CAPTCHA_KEY=\nHQ_CAPTCHA_PROVIDER=2captcha\n\n# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\n# Server-side SEULEMENT : jamais exposée au frontend ni écrite dans les logs.\nSERPAPI_API_KEY=\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1079,"cache_read_input_tokens":135623,"cache_creation":{"ephemeral_5m_input_tokens":1079,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a48398b0-6416-4fda-a9b7-f42e659e36ce"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DJHVnVaTwafVh6E2c5DUMW","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/.env.example"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0eb679fa-bf1e-4c9d-a3ae-b1405e05b099","timestamp":"2026-08-24T21:51:58.367Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/.env.example","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — variables d'environnement (modèle — copier vers .env, NE PAS COMMITTER .env)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# -----------------------------------------------------------------------------\n\n# Connecteurs / scraping\nFIRECRAWL_API_KEY=\nSCRAPFLY_KEY=\nAPIFY_TOKEN=\nOXYLABS_PROXY_USER=\nOXYLABS_PROXY_PASS=\nBRIGHTDATA_API_KEY=\nBRIGHTDATA_ZONE=\n\n# Session / auth\nSESSION_SECRET=\nLOUKA_BASE_URL=https://www.lou-ka.com\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nKA_SSO_SECRET=\nKA_HUB_URL=https://www.groupe-ka.com\n\n# Hydro-Québec (estimation électricité — solveur reCAPTCHA)\nHQ_CAPTCHA_KEY=\nHQ_CAPTCHA_PROVIDER=2captcha\n\n# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\n# Server-side SEULEMENT : jamais exposée au frontend ni écrite dans les logs.\nSERPAPI_API_KEY=\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"37712156-b7d7-48cb-86f1-69e822f71f18","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKC26NDsj6spiJ2CJME","type":"message","role":"assistant","content":[{"type":"text","text":"Étape 2 — migrations de schéma dans `db.py` (nouvelles tables + colonne `building_key`), puis détection d'événements dans `sync_source`."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1047,"cache_read_input_tokens":136702,"cache_creation":{"ephemeral_5m_input_tokens":1047,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"12bb18fd-26ab-4241-9109-8b729fa93854"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKC26NDsj6spiJ2CJME","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NuvnTS4STezEaJ5KKkj6CL","name":"TodoWrite","input":{"todos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"completed","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"in_progress","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"pending","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1047,"cache_read_input_tokens":136702,"cache_creation":{"ephemeral_5m_input_tokens":1047,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"91db9925-0fe4-4221-a268-c28d03ec0f6c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NuvnTS4STezEaJ5KKkj6CL","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e649f502-4c1d-405b-a5f1-7e7295479513","timestamp":"2026-08-24T21:52:13.744Z","tool_use_result":{"oldTodos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"in_progress","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"pending","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"pending","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}],"newTodos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"completed","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"in_progress","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"pending","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKC26NDsj6spiJ2CJME","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KEc3amnE69TBaTcocN1aMc","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":"CREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\"\"\"","new_string":"CREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\""},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1047,"cache_read_input_tokens":136702,"cache_creation":{"ephemeral_5m_input_tokens":1047,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5d19a1bb-efce-4054-9feb-f3a7bf305692"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KEc3amnE69TBaTcocN1aMc","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e105a951-502e-4913-9852-d9a8ced5b85e","timestamp":"2026-08-24T21:52:30.855Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":"CREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\"\"\"","newString":"CREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":174,"oldLines":6,"newStart":174,"newLines":80,"lines":[" logo_url TEXT,"," updated_at REAL"," );","+","+CREATE TABLE IF NOT EXISTS listing_events (","+ id INTEGER PRIMARY KEY AUTOINCREMENT,","+ uid TEXT NOT NULL, -- listings.uid","+ ts REAL NOT NULL,","+ event TEXT NOT NULL, -- description | superficie | dispo |","+ -- inclusions | photos | disparition |","+ -- reapparition (prix -> price_log)","+ old TEXT, -- valeur avant (JSON compact)","+ new TEXT -- valeur après (JSON compact)","+);","+CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);","+","+CREATE TABLE IF NOT EXISTS buildings (","+ bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »","+ -- (dedup._parse_address) ou « geo:lat,lng »","+ address TEXT, -- adresse d'affichage (la plus fréquente)","+ city TEXT,","+ lat REAL, lng REAL,","+ stats TEXT, -- JSON : unités, annonces, loyers,","+ -- rotation, pression, gestionnaires…","+ version TEXT, -- version du calcul (building.py)","+ computed_at REAL","+);","+","+CREATE TABLE IF NOT EXISTS recycled_cache (","+ uid TEXT PRIMARY KEY, -- listings.uid","+ matches TEXT, -- JSON : correspondances probables + signaux","+ computed_at REAL","+);","+","+CREATE TABLE IF NOT EXISTS managers (","+ id INTEGER PRIMARY KEY AUTOINCREMENT,","+ source_id TEXT UNIQUE, -- id de la source (data/sources.json)","+ name TEXT, -- nom canonique","+ aliases TEXT, -- JSON : variantes de nom","+ website TEXT,","+ phone TEXT,","+ -- résolution Google Maps (SerpApi) — voir louka/managers.py","+ gmaps_place_id TEXT,","+ gmaps_data_id TEXT,","+ gmaps_name TEXT,","+ gmaps_address TEXT,","+ gmaps_rating REAL,","+ gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google","+ match_confidence REAL, -- 0..1 (association refusée si trop faible)","+ match_signals TEXT, -- JSON : signaux ayant fondé l'association","+ resolved_at REAL,","+ resolve_failed INTEGER DEFAULT 0,","+ review_stats TEXT, -- JSON : distribution, tendance, thèmes","+ last_synced_at REAL","+);","+","+CREATE TABLE IF NOT EXISTS manager_reviews (","+ id INTEGER PRIMARY KEY AUTOINCREMENT,","+ manager_id INTEGER NOT NULL,","+ external_review_id TEXT, -- id Google (déduplication)","+ source TEXT DEFAULT 'google_maps',","+ rating REAL,","+ text TEXT,","+ published_at TEXT, -- ISO si dérivable","+ relative_date_raw TEXT, -- « il y a 3 mois » (texte source)","+ author_name TEXT,","+ author_review_count INTEGER,","+ owner_response TEXT,","+ owner_response_date TEXT,","+ source_url TEXT,","+ fetched_at REAL,","+ content_hash TEXT, -- déduplication de secours","+ analysis TEXT -- JSON : topics/sentiment/severity (managers.py)","+);","+CREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);","+CREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext","+ ON manager_reviews(manager_id, external_review_id);"," \"\"\""," "," # Colonnes ajoutées après la v1 — migration automatique des bases existantes."]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"e32b00fd-0b57-409e-8e06-68e9a3839f2a","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKER5Y1exKBK1aZsdvy","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PuvNkPzk5LbwL7PPoSMN9B","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":" \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n },","new_string":" \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3132,"cache_read_input_tokens":137749,"cache_creation":{"ephemeral_5m_input_tokens":3132,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1f453afc-bcce-46d2-93da-92d1f219d6f1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PuvNkPzk5LbwL7PPoSMN9B","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"89485051-2aea-42f3-b621-44d53c558dfe","timestamp":"2026-08-24T21:52:37.502Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":" \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n },","newString":" \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":272,"oldLines":6,"newStart":272,"newLines":7,"lines":[" \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)"," \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images"," \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)","+ \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)"," },"," \"sync_log\": {"," \"stats\": \"TEXT\","]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKER5Y1exKBK1aZsdvy","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DXohcsV7jGFycmJbj3Gtrh","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":" con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")","new_string":" con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"\n \" ON listings(building_key)\")"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3132,"cache_read_input_tokens":137749,"cache_creation":{"ephemeral_5m_input_tokens":3132,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9ef6e30e-245a-4f3d-97bc-e5f02957c0a0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DXohcsV7jGFycmJbj3Gtrh","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7bf56f57-8288-4757-a43c-ea46f8ae97b8","timestamp":"2026-08-24T21:52:39.173Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":" con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")","newString":" con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"\n \" ON listings(building_key)\")","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":310,"oldLines":6,"newStart":310,"newLines":8,"lines":[" \" ON listings(active, published)\")"," con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\""," \" ON listings(coord_key)\")","+ con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"","+ \" ON listings(building_key)\")"," con.commit()"," # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans"," # verrou global ; busy_timeout évite les « database is locked » ponctuels."]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"4ee088b5-dab8-4125-b06f-5e493704ab92","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKF2dh8eBUweXZgK4p1","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISsggKpwEIERgCKkDvQYCaP4Lm8ZqzXhnMLPaPOfPu13K3NS9vZbmR7mozvDocagSk/crYsn0ZC3LkjgPt0sT97f7cwbQNN8ea31d2Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG1+rLUBrABAhIMAT09wlLxhRLVi5qPGgwMQaVAU3PFejoW+LoiMKLfi5963zmA9jl2IkQjYbd2rI6hZeXq/q8/uc8YrKZKkdO8q6sHKV90/wdN2iiChCq3Bgo86zeUws/TTkyIqXAvUbatrDLk7v+E3l4Qn5mHcjkIVwSvG43C8kmkcPMpB0NWA3WmSOfifQRiclq9H2hRAWkNpGvxvPlvW0tfsyMsMEwYufWzIXzXEAwc7Jdea3uf0jXtvLs1aR9dQdiSNv4LC56BSATOjm95vFIfxMkkeaWhsb7sP9QKyc5IdAwMkZO21ZCIvh4H0q38XgBaCe3esScz3wmi10oMnp3rbbITF6YKXtnsdy9E5cizSvUjoqkBODycEx6RsXEQ4XMdPOFjkMGzXtWtKl3S1Ynh+FiAMc+YiPXosuUWXT6rFyJqBccYrHSKbu5ORrvEY7T8Ghg8yvGc2Y/cQykSoSdIFKJtoUxfszmKVuH5NRiIJfX0jw5sgsOZD0sSLTfBjivXN8r4LsJ8/US0s/Xtp4+65R3I/JnzMYTo07tslf+1DNjQ/3MrvOJ8/uZkaj6fNk3Xz3L4RF4d1XngXArmx83PWyuID76N/rwKRo9GcQheoALO74+KTnU/hQ0Pd0O2stqnVH3kTGDX5qymHdJNLl0hvb9hi887gTV4yZ1KOaRAGhvdgmTYP9AKRElCNiAlN3y6QTLBszf4ke62MEzU6vYQAaKIbAl+4qXgjYRdlHyYOUEXnffktBCL+vV0ljOel5hHnxD0AnKAe4VEjSb2AizlmcjngEzgS23dzsaNG0hFmziH8qzpDK7HH0FT0dNpVKPK+O8/ZwwGoNjh8IOa5LEo1s95QqWoOom8+q3zgZrD5P0PvLvJIjMZ4x6H9CmEthF4myOwnIGo26uhOJTWmR+OpmcMCcNV2ktfbHO+JLn6lV1OIdWtuK/FaUugatHCiDwBDpXyh6VsUeUluF/bf2xyBuP7BQF8KJLPI4LtAiEM78nvJWPuvyKzBauKIy5M65PMlPSfrqQ3LLvMH+ZL9uHppT+8HYm9XGNM3V6LvnmjN8DzI92n5Yi6KlWeZN7vQ0K2Xr1QApuSwhAdslQEzq6BhtVSgPBce2ZcxLg9k3NBwZluBZUka/nxhCnIaylohQSI2YtDaAfEX0+kYUXHPB7ZiOacYCvMyPWV6Xf+cIv4oktUCq+5IGKXZLkT+HgYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":823,"cache_read_input_tokens":140881,"cache_creation":{"ephemeral_5m_input_tokens":823,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"93df9f39-2522-453c-bb99-47233e3206e5"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKF2dh8eBUweXZgK4p1","type":"message","role":"assistant","content":[{"type":"text","text":"Maintenant la détection d'événements dans `sync_source` (db.py)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":823,"cache_read_input_tokens":140881,"cache_creation":{"ephemeral_5m_input_tokens":823,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"eeb8f918-63d5-4a6f-85b8-ec512153aa8d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKF2dh8eBUweXZgK4p1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SBKsRL6X1tFyGs8a4YWHsd","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":"def sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:","new_string":"def _j(v) -> str:\n return json.dumps(v, ensure_ascii=False)\n\n\ndef _log_events(con: sqlite3.Connection, uid: str, ts: float,\n old: sqlite3.Row, new: dict) -> None:\n \"\"\"Journalise les changements observés d'une annonce (listing_events).\n\n Le prix a déjà son historique dédié (price_log) — ici : description,\n superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n \"\"\"\n ev: list[tuple[str, str | None, str | None]] = []\n old_desc = (old[\"description\"] or \"\").strip()\n new_desc = (new.get(\"description\") or \"\").strip()\n if old_desc != new_desc and (old_desc or new_desc):\n ev.append((\"description\", _j({\"caracteres\": len(old_desc)}),\n _j({\"caracteres\": len(new_desc)})))\n if (old[\"area_sqft\"] or None) != (new.get(\"area_sqft\") or None):\n ev.append((\"superficie\", _j(old[\"area_sqft\"]), _j(new.get(\"area_sqft\"))))\n if (old[\"availability_date\"] or None) != (new.get(\"availability_date\") or None):\n ev.append((\"dispo\", _j(old[\"availability_date\"]),\n _j(new.get(\"availability_date\"))))\n try:\n inc_old = (json.loads(old[\"details\"] or \"{}\") or {}).get(\"inclusions\") or {}\n inc_new = (json.loads(new.get(\"details\") or \"{}\") or {}).get(\"inclusions\") or {}\n except (ValueError, TypeError):\n inc_old = inc_new = {}\n if inc_old != inc_new:\n keys = set(inc_old) | set(inc_new)\n diff_old = {k: inc_old.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n diff_new = {k: inc_new.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n if diff_old or diff_new:\n ev.append((\"inclusions\", _j(diff_old), _j(diff_new)))\n try:\n imgs_old = json.loads(old[\"images\"] or \"[]\")\n imgs_new = json.loads(new.get(\"images\") or \"[]\")\n except (ValueError, TypeError):\n imgs_old = imgs_new = []\n if set(imgs_old) != set(imgs_new):\n ev.append((\"photos\", _j({\"n\": len(imgs_old)}), _j({\"n\": len(imgs_new)})))\n if ev:\n con.executemany(\n \"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n [(uid, ts, e, o, n) for e, o, n in ev])\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":823,"cache_read_input_tokens":140881,"cache_creation":{"ephemeral_5m_input_tokens":823,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ff14440b-803e-44c2-8c48-043930d2a187"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SBKsRL6X1tFyGs8a4YWHsd","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2e1cbc6d-f6e4-4d73-b194-2d08625b315f","timestamp":"2026-08-24T21:53:03.719Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":"def sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:","newString":"def _j(v) -> str:\n return json.dumps(v, ensure_ascii=False)\n\n\ndef _log_events(con: sqlite3.Connection, uid: str, ts: float,\n old: sqlite3.Row, new: dict) -> None:\n \"\"\"Journalise les changements observés d'une annonce (listing_events).\n\n Le prix a déjà son historique dédié (price_log) — ici : description,\n superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n \"\"\"\n ev: list[tuple[str, str | None, str | None]] = []\n old_desc = (old[\"description\"] or \"\").strip()\n new_desc = (new.get(\"description\") or \"\").strip()\n if old_desc != new_desc and (old_desc or new_desc):\n ev.append((\"description\", _j({\"caracteres\": len(old_desc)}),\n _j({\"caracteres\": len(new_desc)})))\n if (old[\"area_sqft\"] or None) != (new.get(\"area_sqft\") or None):\n ev.append((\"superficie\", _j(old[\"area_sqft\"]), _j(new.get(\"area_sqft\"))))\n if (old[\"availability_date\"] or None) != (new.get(\"availability_date\") or None):\n ev.append((\"dispo\", _j(old[\"availability_date\"]),\n _j(new.get(\"availability_date\"))))\n try:\n inc_old = (json.loads(old[\"details\"] or \"{}\") or {}).get(\"inclusions\") or {}\n inc_new = (json.loads(new.get(\"details\") or \"{}\") or {}).get(\"inclusions\") or {}\n except (ValueError, TypeError):\n inc_old = inc_new = {}\n if inc_old != inc_new:\n keys = set(inc_old) | set(inc_new)\n diff_old = {k: inc_old.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n diff_new = {k: inc_new.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n if diff_old or diff_new:\n ev.append((\"inclusions\", _j(diff_old), _j(diff_new)))\n try:\n imgs_old = json.loads(old[\"images\"] or \"[]\")\n imgs_new = json.loads(new.get(\"images\") or \"[]\")\n except (ValueError, TypeError):\n imgs_old = imgs_new = []\n if set(imgs_old) != set(imgs_new):\n ev.append((\"photos\", _j({\"n\": len(imgs_old)}), _j({\"n\": len(imgs_new)})))\n if ev:\n con.executemany(\n \"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n [(uid, ts, e, o, n) for e, o, n in ev])\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"\n \" ON listings(building_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":353,"oldLines":6,"newStart":353,"newLines":54,"lines":[" return None"," "," ","+def _j(v) -> str:","+ return json.dumps(v, ensure_ascii=False)","+","+","+def _log_events(con: sqlite3.Connection, uid: str, ts: float,","+ old: sqlite3.Row, new: dict) -> None:","+ \"\"\"Journalise les changements observés d'une annonce (listing_events).","+","+ Le prix a déjà son historique dédié (price_log) — ici : description,","+ superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de","+ texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.","+ \"\"\"","+ ev: list[tuple[str, str | None, str | None]] = []","+ old_desc = (old[\"description\"] or \"\").strip()","+ new_desc = (new.get(\"description\") or \"\").strip()","+ if old_desc != new_desc and (old_desc or new_desc):","+ ev.append((\"description\", _j({\"caracteres\": len(old_desc)}),","+ _j({\"caracteres\": len(new_desc)})))","+ if (old[\"area_sqft\"] or None) != (new.get(\"area_sqft\") or None):","+ ev.append((\"superficie\", _j(old[\"area_sqft\"]), _j(new.get(\"area_sqft\"))))","+ if (old[\"availability_date\"] or None) != (new.get(\"availability_date\") or None):","+ ev.append((\"dispo\", _j(old[\"availability_date\"]),","+ _j(new.get(\"availability_date\"))))","+ try:","+ inc_old = (json.loads(old[\"details\"] or \"{}\") or {}).get(\"inclusions\") or {}","+ inc_new = (json.loads(new.get(\"details\") or \"{}\") or {}).get(\"inclusions\") or {}","+ except (ValueError, TypeError):","+ inc_old = inc_new = {}","+ if inc_old != inc_new:","+ keys = set(inc_old) | set(inc_new)","+ diff_old = {k: inc_old.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}","+ diff_new = {k: inc_new.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}","+ if diff_old or diff_new:","+ ev.append((\"inclusions\", _j(diff_old), _j(diff_new)))","+ try:","+ imgs_old = json.loads(old[\"images\"] or \"[]\")","+ imgs_new = json.loads(new.get(\"images\") or \"[]\")","+ except (ValueError, TypeError):","+ imgs_old = imgs_new = []","+ if set(imgs_old) != set(imgs_new):","+ ev.append((\"photos\", _j({\"n\": len(imgs_old)}), _j({\"n\": len(imgs_new)})))","+ if ev:","+ con.executemany(","+ \"INSERT INTO listing_events (uid, ts, event, old, new)\"","+ \" VALUES (?,?,?,?,?)\",","+ [(uid, ts, e, o, n) for e, o, n in ev])","+","+"," def sync_source(con: sqlite3.Connection, source: str,"," listings: list[Listing]) -> dict:"," \"\"\"Synchronise les annonces d'une source."]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"ab70559e-6ef1-4f52-a093-d0a7e389032b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKGqZF4PrSi2GUq9Da8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Bhbv1H2xMSJmBgz7ZP5RzZ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":" row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()","new_string":" row = con.execute(\n \"SELECT content_hash, price, images, description, area_sqft,\"\n \" availability_date, details, active FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1580,"cache_read_input_tokens":141704,"cache_creation":{"ephemeral_5m_input_tokens":1580,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"77a3bbca-4200-40c6-8981-cc15e6d82947"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Bhbv1H2xMSJmBgz7ZP5RzZ","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"88600fc0-675a-4c53-bf5d-edd4f02b07f3","timestamp":"2026-08-24T21:53:10.107Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":" row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()","newString":" row = con.execute(\n \"SELECT content_hash, price, images, description, area_sqft,\"\n \" availability_date, details, active FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"\n \" ON listings(building_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef _j(v) -> str:\n return json.dumps(v, ensure_ascii=False)\n\n\ndef _log_events(con: sqlite3.Connection, uid: str, ts: float,\n old: sqlite3.Row, new: dict) -> None:\n \"\"\"Journalise les changements observés d'une annonce (listing_events).\n\n Le prix a déjà son historique dédié (price_log) — ici : description,\n superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n \"\"\"\n ev: list[tuple[str, str | None, str | None]] = []\n old_desc = (old[\"description\"] or \"\").strip()\n new_desc = (new.get(\"description\") or \"\").strip()\n if old_desc != new_desc and (old_desc or new_desc):\n ev.append((\"description\", _j({\"caracteres\": len(old_desc)}),\n _j({\"caracteres\": len(new_desc)})))\n if (old[\"area_sqft\"] or None) != (new.get(\"area_sqft\") or None):\n ev.append((\"superficie\", _j(old[\"area_sqft\"]), _j(new.get(\"area_sqft\"))))\n if (old[\"availability_date\"] or None) != (new.get(\"availability_date\") or None):\n ev.append((\"dispo\", _j(old[\"availability_date\"]),\n _j(new.get(\"availability_date\"))))\n try:\n inc_old = (json.loads(old[\"details\"] or \"{}\") or {}).get(\"inclusions\") or {}\n inc_new = (json.loads(new.get(\"details\") or \"{}\") or {}).get(\"inclusions\") or {}\n except (ValueError, TypeError):\n inc_old = inc_new = {}\n if inc_old != inc_new:\n keys = set(inc_old) | set(inc_new)\n diff_old = {k: inc_old.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n diff_new = {k: inc_new.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n if diff_old or diff_new:\n ev.append((\"inclusions\", _j(diff_old), _j(diff_new)))\n try:\n imgs_old = json.loads(old[\"images\"] or \"[]\")\n imgs_new = json.loads(new.get(\"images\") or \"[]\")\n except (ValueError, TypeError):\n imgs_old = imgs_new = []\n if set(imgs_old) != set(imgs_new):\n ev.append((\"photos\", _j({\"n\": len(imgs_old)}), _j({\"n\": len(imgs_new)})))\n if ev:\n con.executemany(\n \"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n [(uid, ts, e, o, n) for e, o, n in ev])\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":426,"oldLines":7,"newStart":426,"newLines":8,"lines":[" seen_uids.add(lst.uid)"," h = lst.content_hash()"," row = con.execute(","- \"SELECT content_hash, price, images FROM listings WHERE uid=?\",","+ \"SELECT content_hash, price, images, description, area_sqft,\"","+ \" availability_date, details, active FROM listings WHERE uid=?\","," (lst.uid,)).fetchone()"," # score de complétude + seuil de publication (quarantaine sous le seuil)"," from .quality import evaluate"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKGqZF4PrSi2GUq9Da8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0156TQZP9qA3XtYvmZJjVhJw","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":" if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))","new_string":" if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n # timeline de la fiche : changements observés (hors prix)\n _log_events(con, lst.uid, now, row, params)\n if not row[\"active\"]:\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (lst.uid, now, \"reapparition\", None, None))\n updated += 1\n else:\n if not row[\"active\"]:\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (lst.uid, now, \"reapparition\", None, None))\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1580,"cache_read_input_tokens":141704,"cache_creation":{"ephemeral_5m_input_tokens":1580,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"918c36f4-b390-45ff-8245-cdc389e1a7e4"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0156TQZP9qA3XtYvmZJjVhJw","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e31db01f-3a5a-4e65-817c-835a9c25d6ec","timestamp":"2026-08-24T21:53:16.586Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":" if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))","newString":" if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n # timeline de la fiche : changements observés (hors prix)\n _log_events(con, lst.uid, now, row, params)\n if not row[\"active\"]:\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (lst.uid, now, \"reapparition\", None, None))\n updated += 1\n else:\n if not row[\"active\"]:\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (lst.uid, now, \"reapparition\", None, None))\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"\n \" ON listings(building_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef _j(v) -> str:\n return json.dumps(v, ensure_ascii=False)\n\n\ndef _log_events(con: sqlite3.Connection, uid: str, ts: float,\n old: sqlite3.Row, new: dict) -> None:\n \"\"\"Journalise les changements observés d'une annonce (listing_events).\n\n Le prix a déjà son historique dédié (price_log) — ici : description,\n superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n \"\"\"\n ev: list[tuple[str, str | None, str | None]] = []\n old_desc = (old[\"description\"] or \"\").strip()\n new_desc = (new.get(\"description\") or \"\").strip()\n if old_desc != new_desc and (old_desc or new_desc):\n ev.append((\"description\", _j({\"caracteres\": len(old_desc)}),\n _j({\"caracteres\": len(new_desc)})))\n if (old[\"area_sqft\"] or None) != (new.get(\"area_sqft\") or None):\n ev.append((\"superficie\", _j(old[\"area_sqft\"]), _j(new.get(\"area_sqft\"))))\n if (old[\"availability_date\"] or None) != (new.get(\"availability_date\") or None):\n ev.append((\"dispo\", _j(old[\"availability_date\"]),\n _j(new.get(\"availability_date\"))))\n try:\n inc_old = (json.loads(old[\"details\"] or \"{}\") or {}).get(\"inclusions\") or {}\n inc_new = (json.loads(new.get(\"details\") or \"{}\") or {}).get(\"inclusions\") or {}\n except (ValueError, TypeError):\n inc_old = inc_new = {}\n if inc_old != inc_new:\n keys = set(inc_old) | set(inc_new)\n diff_old = {k: inc_old.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n diff_new = {k: inc_new.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n if diff_old or diff_new:\n ev.append((\"inclusions\", _j(diff_old), _j(diff_new)))\n try:\n imgs_old = json.loads(old[\"images\"] or \"[]\")\n imgs_new = json.loads(new.get(\"images\") or \"[]\")\n except (ValueError, TypeError):\n imgs_old = imgs_new = []\n if set(imgs_old) != set(imgs_new):\n ev.append((\"photos\", _j({\"n\": len(imgs_old)}), _j({\"n\": len(imgs_new)})))\n if ev:\n con.executemany(\n \"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n [(uid, ts, e, o, n) for e, o, n in ev])\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images, description, area_sqft,\"\n \" availability_date, details, active FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n updated += 1\n else:\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":508,"oldLines":8,"newStart":508,"newLines":18,"lines":[" if lst.price != row[\"price\"]: # changement de prix -> historique"," con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\","," (lst.uid, now, lst.price))","+ # timeline de la fiche : changements observés (hors prix)","+ _log_events(con, lst.uid, now, row, params)","+ if not row[\"active\"]:","+ con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"","+ \" VALUES (?,?,?,?,?)\",","+ (lst.uid, now, \"reapparition\", None, None))"," updated += 1"," else:","+ if not row[\"active\"]:","+ con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"","+ \" VALUES (?,?,?,?,?)\",","+ (lst.uid, now, \"reapparition\", None, None))"," con.execute("," \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\","," (now, lst.uid))"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"29b1c973-2899-495e-89ca-494ae4e6d712","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKHnSzo6Qc63CGAZxdF","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01143rhS3QCxiv6vBa3vATrF","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","old_string":" if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1","new_string":" if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (r[\"uid\"], now, \"disparition\", None, None))\n removed += 1"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1265,"cache_read_input_tokens":143284,"cache_creation":{"ephemeral_5m_input_tokens":1265,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c248dad4-cdf3-4d73-a139-72cf858e46ef"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01143rhS3QCxiv6vBa3vATrF","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/db.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"30861d5e-1354-45e5-848a-b3ac09b0b977","timestamp":"2026-08-24T21:53:22.466Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","oldString":" if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1","newString":" if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (r[\"uid\"], now, \"disparition\", None, None))\n removed += 1","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# db.py : persistance SQLite — upsert avec détection de changements,\n# cycle de vie avec délai de grâce (2 syncs), détection de dérive,\n# cache des pages détail, cache de géocodage.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport statistics\nimport time\nfrom pathlib import Path\n\nfrom .schema import Listing\n\nDB_PATH = Path(__file__).resolve().parent.parent / \"data\" / \"louka.db\"\n\n# Nombre d'exécutions consécutives où une annonce doit être absente de la\n# source avant d'être désactivée (délai de grâce contre les ratés ponctuels).\nMISS_GRACE = 2\n\n# Dérive : si une source retourne <= DRIFT_RATIO × sa médiane historique\n# (médiane >= DRIFT_MIN_BASE annonces), on alerte et on suspend les retraits.\nDRIFT_RATIO = 0.25\nDRIFT_MIN_BASE = 8\nDRIFT_HISTORY = 5\n\n_SCHEMA = \"\"\"\nCREATE TABLE IF NOT EXISTS listings (\n uid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n url TEXT,\n title TEXT,\n address TEXT,\n sector TEXT,\n city TEXT,\n unit_type TEXT,\n bedrooms REAL,\n bathrooms REAL,\n price REAL,\n price_label TEXT,\n availability TEXT,\n availability_date TEXT,\n area_sqft REAL,\n pets TEXT,\n furnished INTEGER,\n description TEXT,\n amenities TEXT, -- JSON (liste de textes source)\n details TEXT, -- JSON (champs structurés : inclusions, parking…)\n images TEXT, -- JSON\n lat REAL,\n lng REAL,\n geocode_failed INTEGER DEFAULT 0,\n content_hash TEXT,\n first_seen REAL,\n last_seen REAL,\n updated_at REAL,\n miss_count INTEGER DEFAULT 0,\n active INTEGER DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_listings_source ON listings(source);\nCREATE INDEX IF NOT EXISTS idx_listings_city ON listings(city);\nCREATE INDEX IF NOT EXISTS idx_listings_active ON listings(active);\nCREATE INDEX IF NOT EXISTS idx_listings_geo ON listings(lat, lng);\n\nCREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT,\n ts REAL,\n found INTEGER,\n added INTEGER,\n updated INTEGER,\n removed INTEGER,\n ok INTEGER,\n message TEXT,\n stats TEXT -- JSON : taux de champs null, missed, alerte…\n);\n\nCREATE TABLE IF NOT EXISTS detail_cache (\n source TEXT NOT NULL,\n external_id TEXT NOT NULL,\n key TEXT, -- hash du contenu « liste » de l'annonce\n payload TEXT, -- JSON opaque propre au connecteur\n fetched_at REAL,\n PRIMARY KEY (source, external_id)\n);\n\nCREATE TABLE IF NOT EXISTS price_log (\n uid TEXT NOT NULL,\n ts REAL NOT NULL,\n price REAL -- prix observé (NULL = retiré de l'affichage)\n);\nCREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n\nCREATE TABLE IF NOT EXISTS poi_cache (\n coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n lat REAL,\n lng REAL,\n pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS geocode_cache (\n address TEXT PRIMARY KEY, -- adresse normalisée (clé de cache)\n lat REAL,\n lng REAL,\n provider TEXT,\n failed INTEGER DEFAULT 0,\n ts REAL\n);\n\nCREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n google_sub TEXT UNIQUE, -- identifiant stable Google (OpenID « sub »)\n ka_id TEXT, -- identifiant membre « ka-0123456789 » (unique)\n email TEXT,\n name TEXT,\n picture TEXT, -- URL de l'avatar Google\n public INTEGER DEFAULT 0, -- profil public /u/{ka_id} (opt-in)\n role TEXT, -- « locataire » | « gestionnaire » | NULL\n org_source TEXT, -- source réclamée (gestionnaire)\n created_at REAL,\n last_login REAL\n);\n\nCREATE TABLE IF NOT EXISTS favorites (\n user_id INTEGER NOT NULL,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL,\n PRIMARY KEY (user_id, uid)\n);\n\nCREATE TABLE IF NOT EXISTS image_checks (\n url TEXT PRIMARY KEY, -- URL d'image vérifiée (imgcheck.py)\n status TEXT, -- ok | dead | small | error\n width INTEGER,\n height INTEGER,\n bytes INTEGER,\n sha1 TEXT, -- hash du contenu (doublons / placeholders)\n checked_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS env_tiles (\n tile_key TEXT PRIMARY KEY, -- « ty,tx » (tuiles 0,5° — voir poi.py/environment.py)\n data TEXT, -- JSON : routes/rails/aéro/industriel/bars/cyclable/POI\n fetched_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS kascores (\n coord_key TEXT PRIMARY KEY, -- « lat,lng » arrondi à 4 décimales (immeuble)\n lat REAL, lng REAL,\n walk REAL, -- KA Walk Score 0-100 (NULL = données insuffisantes)\n transit REAL, -- KA Transit Score (NULL = non desservi/inconnu)\n bike REAL,\n calme REAL,\n services REAL,\n global REAL, -- moyenne pondérée par défaut (voir kascores.py)\n details TEXT, -- JSON : détail par score (fiche + méthodologie)\n version TEXT, -- version du barème (recalcul si changement)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS source_profiles (\n source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\n owner_user_id INTEGER, -- gestionnaire qui a réclamé la page\n tagline TEXT, -- accroche personnalisée\n description TEXT,\n website TEXT,\n phone TEXT,\n email TEXT,\n logo_url TEXT,\n updated_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS listing_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n uid TEXT NOT NULL, -- listings.uid\n ts REAL NOT NULL,\n event TEXT NOT NULL, -- description | superficie | dispo |\n -- inclusions | photos | disparition |\n -- reapparition (prix -> price_log)\n old TEXT, -- valeur avant (JSON compact)\n new TEXT -- valeur après (JSON compact)\n);\nCREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n\nCREATE TABLE IF NOT EXISTS buildings (\n bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n -- (dedup._parse_address) ou « geo:lat,lng »\n address TEXT, -- adresse d'affichage (la plus fréquente)\n city TEXT,\n lat REAL, lng REAL,\n stats TEXT, -- JSON : unités, annonces, loyers,\n -- rotation, pression, gestionnaires…\n version TEXT, -- version du calcul (building.py)\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS recycled_cache (\n uid TEXT PRIMARY KEY, -- listings.uid\n matches TEXT, -- JSON : correspondances probables + signaux\n computed_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS managers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n name TEXT, -- nom canonique\n aliases TEXT, -- JSON : variantes de nom\n website TEXT,\n phone TEXT,\n -- résolution Google Maps (SerpApi) — voir louka/managers.py\n gmaps_place_id TEXT,\n gmaps_data_id TEXT,\n gmaps_name TEXT,\n gmaps_address TEXT,\n gmaps_rating REAL,\n gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n match_confidence REAL, -- 0..1 (association refusée si trop faible)\n match_signals TEXT, -- JSON : signaux ayant fondé l'association\n resolved_at REAL,\n resolve_failed INTEGER DEFAULT 0,\n review_stats TEXT, -- JSON : distribution, tendance, thèmes\n last_synced_at REAL\n);\n\nCREATE TABLE IF NOT EXISTS manager_reviews (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n manager_id INTEGER NOT NULL,\n external_review_id TEXT, -- id Google (déduplication)\n source TEXT DEFAULT 'google_maps',\n rating REAL,\n text TEXT,\n published_at TEXT, -- ISO si dérivable\n relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n author_name TEXT,\n author_review_count INTEGER,\n owner_response TEXT,\n owner_response_date TEXT,\n source_url TEXT,\n fetched_at REAL,\n content_hash TEXT, -- déduplication de secours\n analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n);\nCREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n ON manager_reviews(manager_id, external_review_id);\n\"\"\"\n\n# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n_MIGRATIONS = {\n \"listings\": {\n \"availability_date\": \"TEXT\",\n \"area_sqft\": \"REAL\",\n \"pets\": \"TEXT\",\n \"furnished\": \"INTEGER\",\n \"details\": \"TEXT\",\n \"geocode_failed\": \"INTEGER DEFAULT 0\",\n \"miss_count\": \"INTEGER DEFAULT 0\",\n \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)\n \"quality_reasons\": \"TEXT\", # JSON : raisons de la quarantaine\n \"images_ok\": \"TEXT\", # JSON : galerie nettoyée (imgcheck.py)\n \"img_audit\": \"TEXT\", # JSON : traçabilité du contrôle images\n \"coord_key\": \"TEXT\", # clé immeuble « lat,lng » 4 déc. (jointure kascores)\n \"building_key\": \"TEXT\", # clé immeuble par adresse (buildings.bkey)\n },\n \"sync_log\": {\n \"stats\": \"TEXT\",\n },\n \"users\": {\n \"ka_id\": \"TEXT\",\n \"public\": \"INTEGER DEFAULT 0\",\n \"role\": \"TEXT\",\n \"org_source\": \"TEXT\",\n \"display_name\": \"TEXT\", # nom choisi (prime sur le nom Google)\n \"avatar_url\": \"TEXT\", # photo téléversée (prime sur picture)\n \"bio\": \"TEXT\",\n \"city\": \"TEXT\",\n \"phone\": \"TEXT\",\n \"website\": \"TEXT\",\n \"socials\": \"TEXT\", # JSON {instagram, facebook, x, linkedin…}\n },\n}\n\n\ndef connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH)\n con.row_factory = sqlite3.Row\n con.executescript(_SCHEMA)\n for table, cols in _MIGRATIONS.items():\n existing = {r[\"name\"] for r in con.execute(f\"PRAGMA table_info({table})\")}\n for col, decl in cols.items():\n if col not in existing:\n con.execute(f\"ALTER TABLE {table} ADD COLUMN {col} {decl}\")\n # index sur des colonnes de migration : APRÈS les ALTER TABLE (une base\n # antérieure n'a pas encore la colonne au moment du CREATE INDEX)\n con.execute(\"CREATE UNIQUE INDEX IF NOT EXISTS users_ka_id ON users(ka_id)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_pub\"\n \" ON listings(active, published)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_coord\"\n \" ON listings(coord_key)\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_listings_bkey\"\n \" ON listings(building_key)\")\n con.commit()\n # WAL : lectures (web) et écritures (sync, imgcheck) concurrentes sans\n # verrou global ; busy_timeout évite les « database is locked » ponctuels.\n con.execute(\"PRAGMA journal_mode=WAL\")\n con.execute(\"PRAGMA busy_timeout=30000\")\n return con\n\n\n# ---------------------------------------------------------------------------\n# Synchronisation d'une source\n# ---------------------------------------------------------------------------\n\ndef _drift_alert(con: sqlite3.Connection, source: str, found: int,\n null_price_rate: float) -> str | None:\n \"\"\"Détecte une dérive du connecteur (chute du volume ou des prix extraits).\n\n Retourne un message d'alerte, ou None si tout est normal.\n \"\"\"\n hist = con.execute(\n \"SELECT found, stats FROM sync_log WHERE source=? AND ok=1\"\n \" ORDER BY ts DESC LIMIT ?\", (source, DRIFT_HISTORY)).fetchall()\n if len(hist) < 3:\n return None\n med_found = statistics.median(r[\"found\"] for r in hist)\n if med_found >= DRIFT_MIN_BASE and found <= DRIFT_RATIO * med_found:\n return (f\"dérive: {found} annonce(s) trouvée(s) contre une médiane de \"\n f\"{med_found:.0f} — retraits suspendus, vérifier le connecteur\")\n if found >= DRIFT_MIN_BASE and null_price_rate >= 0.8:\n rates = []\n for r in hist:\n try:\n rates.append(json.loads(r[\"stats\"] or \"{}\")[\"null_price_rate\"])\n except (KeyError, ValueError, TypeError):\n continue\n if rates and statistics.median(rates) <= 0.3:\n return (f\"dérive: {null_price_rate:.0%} des annonces sans prix \"\n f\"(habituellement {statistics.median(rates):.0%}) — \"\n \"le format de la source a probablement changé\")\n return None\n\n\ndef _j(v) -> str:\n return json.dumps(v, ensure_ascii=False)\n\n\ndef _log_events(con: sqlite3.Connection, uid: str, ts: float,\n old: sqlite3.Row, new: dict) -> None:\n \"\"\"Journalise les changements observés d'une annonce (listing_events).\n\n Le prix a déjà son historique dédié (price_log) — ici : description,\n superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n \"\"\"\n ev: list[tuple[str, str | None, str | None]] = []\n old_desc = (old[\"description\"] or \"\").strip()\n new_desc = (new.get(\"description\") or \"\").strip()\n if old_desc != new_desc and (old_desc or new_desc):\n ev.append((\"description\", _j({\"caracteres\": len(old_desc)}),\n _j({\"caracteres\": len(new_desc)})))\n if (old[\"area_sqft\"] or None) != (new.get(\"area_sqft\") or None):\n ev.append((\"superficie\", _j(old[\"area_sqft\"]), _j(new.get(\"area_sqft\"))))\n if (old[\"availability_date\"] or None) != (new.get(\"availability_date\") or None):\n ev.append((\"dispo\", _j(old[\"availability_date\"]),\n _j(new.get(\"availability_date\"))))\n try:\n inc_old = (json.loads(old[\"details\"] or \"{}\") or {}).get(\"inclusions\") or {}\n inc_new = (json.loads(new.get(\"details\") or \"{}\") or {}).get(\"inclusions\") or {}\n except (ValueError, TypeError):\n inc_old = inc_new = {}\n if inc_old != inc_new:\n keys = set(inc_old) | set(inc_new)\n diff_old = {k: inc_old.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n diff_new = {k: inc_new.get(k) for k in keys if inc_old.get(k) != inc_new.get(k)}\n if diff_old or diff_new:\n ev.append((\"inclusions\", _j(diff_old), _j(diff_new)))\n try:\n imgs_old = json.loads(old[\"images\"] or \"[]\")\n imgs_new = json.loads(new.get(\"images\") or \"[]\")\n except (ValueError, TypeError):\n imgs_old = imgs_new = []\n if set(imgs_old) != set(imgs_new):\n ev.append((\"photos\", _j({\"n\": len(imgs_old)}), _j({\"n\": len(imgs_new)})))\n if ev:\n con.executemany(\n \"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n [(uid, ts, e, o, n) for e, o, n in ev])\n\n\ndef sync_source(con: sqlite3.Connection, source: str,\n listings: list[Listing]) -> dict:\n \"\"\"Synchronise les annonces d'une source.\n\n - nouvelle annonce -> insertion\n - annonce modifiée -> mise à jour (comparaison de content_hash)\n - annonce disparue -> miss_count += 1, puis active=0 après MISS_GRACE\n exécutions consécutives (délai de grâce)\n - dérive détectée -> alerte consignée, retraits suspendus\n \"\"\"\n now = time.time()\n added = updated = 0\n seen_uids = set()\n\n n = len(listings)\n null_price = sum(1 for l in listings if l.price is None)\n null_addr = sum(1 for l in listings if not l.address)\n null_price_rate = round(null_price / n, 3) if n else 0.0\n\n alert = _drift_alert(con, source, n, null_price_rate)\n\n for lst in listings:\n seen_uids.add(lst.uid)\n h = lst.content_hash()\n row = con.execute(\n \"SELECT content_hash, price, images, description, area_sqft,\"\n \" availability_date, details, active FROM listings WHERE uid=?\",\n (lst.uid,)).fetchone()\n # score de complétude + seuil de publication (quarantaine sous le seuil)\n from .quality import evaluate\n q_score, q_pub, q_reasons = evaluate({\n \"title\": lst.title,\n \"price\": lst.price, \"address\": lst.address, \"city\": lst.city,\n \"sector\": lst.sector, \"lat\": lst.lat, \"lng\": lst.lng,\n \"description\": lst.description, \"images\": lst.images,\n \"unit_type\": lst.unit_type, \"bedrooms\": lst.bedrooms,\n \"bathrooms\": lst.bathrooms, \"availability\": lst.availability,\n \"availability_date\": lst.availability_date,\n \"area_sqft\": lst.area_sqft, \"amenities\": lst.amenities,\n \"details\": lst.details, \"url\": lst.url})\n params = dict(\n uid=lst.uid, source=lst.source, external_id=lst.external_id,\n url=lst.url, title=lst.title, address=lst.address,\n sector=lst.sector, city=lst.city, unit_type=lst.unit_type,\n bedrooms=lst.bedrooms, bathrooms=lst.bathrooms,\n price=lst.price, price_label=lst.price_label,\n availability=lst.availability,\n availability_date=lst.availability_date,\n area_sqft=lst.area_sqft, pets=lst.pets,\n furnished=(None if lst.furnished is None else int(lst.furnished)),\n description=lst.description,\n digest=(json.dumps(lst.digest, ensure_ascii=False)\n if getattr(lst, \"digest\", None) else None),\n amenities=json.dumps(lst.amenities, ensure_ascii=False),\n details=json.dumps(lst.details, ensure_ascii=False),\n images=json.dumps(lst.images, ensure_ascii=False),\n lat=lst.lat, lng=lst.lng, content_hash=h, now=now,\n completeness=q_score, published=int(q_pub),\n quality_reasons=(json.dumps(q_reasons, ensure_ascii=False)\n if q_reasons else None),\n )\n if row is None:\n con.execute(\n \"\"\"INSERT INTO listings (uid, source, external_id, url, title,\n address, sector, city, unit_type, bedrooms, bathrooms,\n price, price_label,\n availability, availability_date, area_sqft, pets, furnished,\n description, digest, amenities, details, images, lat, lng,\n content_hash, first_seen, last_seen, updated_at,\n miss_count, active, completeness, published,\n quality_reasons)\n VALUES (:uid,:source,:external_id,:url,:title,:address,\n :sector,:city,:unit_type,:bedrooms,:bathrooms,\n :price,:price_label,:availability,\n :availability_date,:area_sqft,:pets,:furnished,\n :description,:digest,:amenities,:details,:images,:lat,:lng,\n :content_hash,:now,:now,:now,0,1,\n :completeness,:published,:quality_reasons)\"\"\", params)\n if lst.price is not None: # prix initial = point de départ de l'historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n added += 1\n elif row[\"content_hash\"] != h:\n # COALESCE : ne jamais écraser des coordonnées géocodées par null\n con.execute(\n \"\"\"UPDATE listings SET url=:url, title=:title,\n address=:address, sector=:sector, city=:city,\n unit_type=:unit_type, bedrooms=:bedrooms,\n bathrooms=:bathrooms, price=:price,\n price_label=:price_label, availability=:availability,\n availability_date=:availability_date,\n area_sqft=:area_sqft, pets=:pets, furnished=:furnished,\n description=:description, digest=:digest,\n amenities=:amenities, details=:details, images=:images,\n lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng),\n content_hash=:content_hash, last_seen=:now,\n updated_at=:now, miss_count=0, active=1,\n completeness=:completeness, published=:published,\n quality_reasons=:quality_reasons\n WHERE uid=:uid\"\"\", params)\n if (row[\"images\"] or \"[]\") != params[\"images\"]:\n # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n \" WHERE uid=?\", (lst.uid,))\n if lst.price != row[\"price\"]: # changement de prix -> historique\n con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n (lst.uid, now, lst.price))\n # timeline de la fiche : changements observés (hors prix)\n _log_events(con, lst.uid, now, row, params)\n if not row[\"active\"]:\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (lst.uid, now, \"reapparition\", None, None))\n updated += 1\n else:\n if not row[\"active\"]:\n con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"\n \" VALUES (?,?,?,?,?)\",\n (lst.uid, now, \"reapparition\", None, None))\n con.execute(\n \"UPDATE listings SET last_seen=?, miss_count=0, active=1 WHERE uid=?\",\n (now, lst.uid))\n\n # Annonces de cette source qui n'apparaissent plus : délai de grâce,\n # puis désactivation. Suspendu si une dérive est détectée.\n removed = missed = 0\n if not alert:\n for r in con.execute(\n \"SELECT uid, miss_count FROM listings WHERE source=? AND active=1\",\n (source,)).fetchall():\n if r[\"uid\"] in seen_uids:\n continue\n missed += 1\n if r[\"miss_count\"] + 1 >= MISS_GRACE:\n con.execute(\n \"UPDATE listings SET active=0, miss_count=?, updated_at=?\"\n \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))\n removed += 1\n else:\n con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\",\n (r[\"uid\"],))\n\n stats = {\n \"null_price_rate\": null_price_rate,\n \"null_address_rate\": round(null_addr / n, 3) if n else 0.0,\n \"missed\": missed,\n }\n if alert:\n stats[\"alert\"] = alert\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok,\"\n \" message, stats) VALUES (?,?,?,?,?,?,1,?,?)\",\n (source, now, n, added, updated, removed, alert or \"ok\",\n json.dumps(stats, ensure_ascii=False)))\n con.commit()\n out = {\"source\": source, \"found\": n, \"added\": added,\n \"updated\": updated, \"removed\": removed}\n if alert:\n out[\"alert\"] = alert\n return out\n\n\ndef log_failure(con: sqlite3.Connection, source: str, message: str) -> None:\n con.execute(\n \"INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)\"\n \" VALUES (?,?,0,0,0,0,0,?)\", (source, time.time(), message))\n con.commit()\n\n\n# ---------------------------------------------------------------------------\n# Cache des pages détail (« détail si nouveau/modifié »)\n# ---------------------------------------------------------------------------\n\ndef get_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str) -> dict | None:\n \"\"\"Payload détail mis en cache si la clé (hash liste) n'a pas changé.\"\"\"\n row = con.execute(\n \"SELECT key, payload FROM detail_cache WHERE source=? AND external_id=?\",\n (source, external_id)).fetchone()\n if row and row[\"key\"] == key and row[\"payload\"]:\n try:\n return json.loads(row[\"payload\"])\n except ValueError:\n return None\n return None\n\n\ndef put_cached_detail(con: sqlite3.Connection, source: str,\n external_id: str, key: str, payload: dict) -> None:\n con.execute(\n \"INSERT INTO detail_cache (source, external_id, key, payload, fetched_at)\"\n \" VALUES (?,?,?,?,?)\"\n \" ON CONFLICT(source, external_id) DO UPDATE SET\"\n \" key=excluded.key, payload=excluded.payload, fetched_at=excluded.fetched_at\",\n (source, external_id, key, json.dumps(payload, ensure_ascii=False),\n time.time()))\n con.commit()\n","structuredPatch":[{"oldStart":538,"oldLines":6,"newStart":538,"newLines":9,"lines":[" con.execute("," \"UPDATE listings SET active=0, miss_count=?, updated_at=?\""," \" WHERE uid=?\", (r[\"miss_count\"] + 1, now, r[\"uid\"]))","+ con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"","+ \" VALUES (?,?,?,?,?)\",","+ (r[\"uid\"], now, \"disparition\", None, None))"," removed += 1"," else:"," con.execute(\"UPDATE listings SET miss_count=miss_count+1 WHERE uid=?\","]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"90e36043-9f92-4fbd-a6ce-6faee7de4665","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKJDgnieuyn6k6bryzj","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS/AQKpwEIERgCKkD/2vG6U4Uin5pmzlGcSv2i4iW5pz4whlaC3vaaLJQou3qcCBdeNvE7784PL0hXIMjXNcywdJAfGh5DZ0giW/C2Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHX+rLUBrABAhIMmPY1RYmn/3GRgzmDGgwyp9DpD/LE6c7SEUciMFaBZLRee8iWqgks2pjLtIPsSWjnUUqB2s16lsbAyQvRmS3ZCbt2HgbBK3scorwCgCqBA6EIGB1n0/NqSkw7/Mj5N7yaVBp0SQ0bk7UJ0/sL5BFwpP3pRrSUO6SchfE7B6XnFROdoCLFHLFbHxqGqF1u4FkAT7SZ98xUHRzLeevmcSFZKvEf1vRqYg3K9Y6Ek/uhKJA7unVYUFBPf9L/YU4zRL0akwB/tf8GxutxhZg2nUzV5h6ENvB8bQGJv1koBrAd2tNY0BKPSsUJPPfsgWCi5tDnZ2gOwQLHsSzu/uiAKTttctsKm3s+IYopVSzlipNA1TpuFaMoqCWAScOfT03hd+mBrCcBe3msyIdhWQSkaLQH4QRFHvzcZejbej+S+vy2Uzx8ymMaPXuGToqRz9//nNYlXuWlwvVBfQZf2faDJ4oaJjU0vqCMChjlda3yAyxTTjPhkwRWqMQI32ZkNXnSsXFakXgKlxmLM3ZQn9BMJ25uG9PK9jbgr3Ii8ZKohyMARbeAzMYdaUk6jdYtYo5y8yaakxPotW475iGePLwvhsLgkFkV/bO2YF918kSiywrPfMMYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":472,"cache_read_input_tokens":144549,"cache_creation":{"ephemeral_5m_input_tokens":472,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0dc9b457-33d2-4a54-9e49-4060a5887a2f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKJDgnieuyn6k6bryzj","type":"message","role":"assistant","content":[{"type":"text","text":"Migrations et journalisation d'événements en place. Étape 3 — module `louka/history.py` (timeline de la fiche)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":472,"cache_read_input_tokens":144549,"cache_creation":{"ephemeral_5m_input_tokens":472,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0c007fb0-4582-47fd-8b76-896b6e34fd7d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKJDgnieuyn6k6bryzj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01LK91fRWmLhXVH4wyxqJXd9","name":"TodoWrite","input":{"todos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"completed","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"completed","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"in_progress","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":472,"cache_read_input_tokens":144549,"cache_creation":{"ephemeral_5m_input_tokens":472,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7e8ee29b-3b73-4a33-8e74-2aa8e9131ba9"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01LK91fRWmLhXVH4wyxqJXd9","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bb68ee07-0394-4b33-ba9f-0b33ad443d67","timestamp":"2026-08-24T21:53:37.215Z","tool_use_result":{"oldTodos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"completed","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"in_progress","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"pending","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}],"newTodos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"completed","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"completed","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"in_progress","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKJDgnieuyn6k6bryzj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01D4j7J1gx9aYj2sbZuZUmsi","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/history.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# history.py : « Historique Lou-Ka » d'une annonce — timeline construite à\n# partir des observations réelles des crawls :\n# - price_log (depuis la v1) : chaque changement de prix observé ;\n# - listing_events (db.py/_log_events) : description, superficie, dispo,\n# inclusions, photos, disparition/réapparition.\n# Chaque élément est étiqueté `observed` : rien n'est estimé ni inventé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport time\n\nfrom . import db\n\n\ndef timeline(uid: str, con=None) -> dict | None:\n \"\"\"Historique complet d'une annonce (fiche + API).\n\n Retourne : première/dernière observation, jours en ligne, prix initial et\n courant, variation, nombre de modifications et la timeline fusionnée\n (prix + événements), du plus récent au plus ancien.\n \"\"\"\n own = con is None\n if own:\n con = db.connect()\n row = con.execute(\n \"SELECT uid, price, first_seen, last_seen, active FROM listings\"\n \" WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n if own:\n con.close()\n return None\n\n prices = con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts\", (uid,)).fetchall()\n events = con.execute(\n \"SELECT ts, event, old, new FROM listing_events WHERE uid=? ORDER BY ts\",\n (uid,)).fetchall()\n\n items: list[dict] = []\n prev_price = None\n for p in prices:\n it = {\"ts\": p[\"ts\"], \"type\": \"prix\", \"prix\": p[\"price\"],\n \"statut\": \"observed\"}\n if prev_price is not None and p[\"price\"] is not None:\n it[\"prix_avant\"] = prev_price\n items.append(it)\n if p[\"price\"] is not None:\n prev_price = p[\"price\"]\n for e in events:\n def _load(v):\n try:\n return json.loads(v) if v is not None else None\n except (ValueError, TypeError):\n return None\n items.append({\"ts\": e[\"ts\"], \"type\": e[\"event\"],\n \"avant\": _load(e[\"old\"]), \"apres\": _load(e[\"new\"]),\n \"statut\": \"observed\"})\n items.sort(key=lambda x: x[\"ts\"], reverse=True)\n\n first_price = next((p[\"price\"] for p in prices if p[\"price\"] is not None), None)\n cur_price = row[\"price\"]\n variation = None\n if first_price and cur_price and first_price > 0:\n variation = round((cur_price - first_price) / first_price, 4)\n\n now = time.time()\n fin = row[\"last_seen\"] if not row[\"active\"] else now\n jours = max(0, round((fin - (row[\"first_seen\"] or fin)) / 86400))\n\n # modifications = éléments de timeline hors point de départ du prix\n n_modif = max(0, len([p for p in prices]) - 1) + len(\n [e for e in events if e[\"event\"] not in (\"disparition\", \"reapparition\")])\n\n out = {\n \"uid\": uid,\n \"premiere_observation\": row[\"first_seen\"],\n \"derniere_observation\": row[\"last_seen\"],\n \"active\": bool(row[\"active\"]),\n \"jours_en_ligne\": jours,\n \"prix_initial\": first_price,\n \"prix_actuel\": cur_price,\n \"variation\": variation,\n \"modifications\": n_modif,\n \"timeline\": items[:60],\n \"methode\": (\"Observations directes des synchronisations Lou-Ka \"\n \"(prix, description, superficie, disponibilité, inclusions, \"\n \"photos, retraits/retours). Aucune estimation.\"),\n }\n if own:\n con.close()\n return out\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":472,"cache_read_input_tokens":144549,"cache_creation":{"ephemeral_5m_input_tokens":472,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"da3365de-08c6-4c55-b589-64181b6907eb"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01D4j7J1gx9aYj2sbZuZUmsi","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/history.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4b31916c-96a1-4258-be67-a6d0f4fe5e5c","timestamp":"2026-08-24T21:53:51.226Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/history.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# history.py : « Historique Lou-Ka » d'une annonce — timeline construite à\n# partir des observations réelles des crawls :\n# - price_log (depuis la v1) : chaque changement de prix observé ;\n# - listing_events (db.py/_log_events) : description, superficie, dispo,\n# inclusions, photos, disparition/réapparition.\n# Chaque élément est étiqueté `observed` : rien n'est estimé ni inventé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport time\n\nfrom . import db\n\n\ndef timeline(uid: str, con=None) -> dict | None:\n \"\"\"Historique complet d'une annonce (fiche + API).\n\n Retourne : première/dernière observation, jours en ligne, prix initial et\n courant, variation, nombre de modifications et la timeline fusionnée\n (prix + événements), du plus récent au plus ancien.\n \"\"\"\n own = con is None\n if own:\n con = db.connect()\n row = con.execute(\n \"SELECT uid, price, first_seen, last_seen, active FROM listings\"\n \" WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n if own:\n con.close()\n return None\n\n prices = con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts\", (uid,)).fetchall()\n events = con.execute(\n \"SELECT ts, event, old, new FROM listing_events WHERE uid=? ORDER BY ts\",\n (uid,)).fetchall()\n\n items: list[dict] = []\n prev_price = None\n for p in prices:\n it = {\"ts\": p[\"ts\"], \"type\": \"prix\", \"prix\": p[\"price\"],\n \"statut\": \"observed\"}\n if prev_price is not None and p[\"price\"] is not None:\n it[\"prix_avant\"] = prev_price\n items.append(it)\n if p[\"price\"] is not None:\n prev_price = p[\"price\"]\n for e in events:\n def _load(v):\n try:\n return json.loads(v) if v is not None else None\n except (ValueError, TypeError):\n return None\n items.append({\"ts\": e[\"ts\"], \"type\": e[\"event\"],\n \"avant\": _load(e[\"old\"]), \"apres\": _load(e[\"new\"]),\n \"statut\": \"observed\"})\n items.sort(key=lambda x: x[\"ts\"], reverse=True)\n\n first_price = next((p[\"price\"] for p in prices if p[\"price\"] is not None), None)\n cur_price = row[\"price\"]\n variation = None\n if first_price and cur_price and first_price > 0:\n variation = round((cur_price - first_price) / first_price, 4)\n\n now = time.time()\n fin = row[\"last_seen\"] if not row[\"active\"] else now\n jours = max(0, round((fin - (row[\"first_seen\"] or fin)) / 86400))\n\n # modifications = éléments de timeline hors point de départ du prix\n n_modif = max(0, len([p for p in prices]) - 1) + len(\n [e for e in events if e[\"event\"] not in (\"disparition\", \"reapparition\")])\n\n out = {\n \"uid\": uid,\n \"premiere_observation\": row[\"first_seen\"],\n \"derniere_observation\": row[\"last_seen\"],\n \"active\": bool(row[\"active\"]),\n \"jours_en_ligne\": jours,\n \"prix_initial\": first_price,\n \"prix_actuel\": cur_price,\n \"variation\": variation,\n \"modifications\": n_modif,\n \"timeline\": items[:60],\n \"methode\": (\"Observations directes des synchronisations Lou-Ka \"\n \"(prix, description, superficie, disponibilité, inclusions, \"\n \"photos, retraits/retours). Aucune estimation.\"),\n }\n if own:\n con.close()\n return out\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"1925e36c-7d89-4c3a-9cd3-80e1e8eff957","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKLLrx48D4FSLDNLXfc","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2780,"cache_read_input_tokens":145021,"cache_creation":{"ephemeral_5m_input_tokens":2780,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"afc13edf-4acb-4a80-8162-0edc9612dd4b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKLLrx48D4FSLDNLXfc","type":"message","role":"assistant","content":[{"type":"text","text":"Étape 4 — `louka/building.py` : le passeport de l'immeuble (rollup persistant, rotation, pression sur les loyers)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2780,"cache_read_input_tokens":145021,"cache_creation":{"ephemeral_5m_input_tokens":2780,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cef40be4-bd9b-4c25-8cda-10dfe85bb14a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKLLrx48D4FSLDNLXfc","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XHX8DXdDUHV5ZQgDeroExH","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/building.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# building.py : « Passeport de l'immeuble » — entité persistante regroupant les\n# annonces (actives ET historiques) d'un même bâtiment.\n#\n# Clé d'immeuble : la clé d'adresse civique de dedup._parse_address\n# (« civique|rue|ville », préfixe « adr: ») — la même logique éprouvée que la\n# déduplication inter-sources ; repli « geo:coord_key » (~11 m) quand l'adresse\n# n'a pas de numéro civique fiable mais que l'annonce est géocodée.\n#\n# Tout indicateur est calculé UNIQUEMENT sur ce que Lou-Ka observe réellement\n# (annonces collectées par les crawls). Un indicateur sans échantillon\n# suffisant est omis ou marqué « donnees_insuffisantes » — jamais inventé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport statistics\nimport time\n\nfrom . import db\nfrom .dedup import CLASSIFIEDS, PORTALS, _parse_address\n\nVERSION = \"immeuble-1.0\"\n\n# seuils du score de rotation (annonces des 12 derniers mois / unités estimées)\nROTATION = [(0.5, \"faible\"), (1.0, \"normale\"), (2.0, \"élevée\")]\nROTATION_MAX = \"très élevée\"\nMIN_OBS_DAYS = 180 # fenêtre d'observation minimale pour classer la rotation\nMIN_ADS_ROTATION = 3 # nb d'annonces minimal pour classer la rotation\nMIN_ADS_PRESSURE = 3 # nb minimal par fenêtre pour la pression sur les loyers\n\n\ndef building_key(address: str | None, city: str | None,\n coord_key: str | None) -> tuple[str | None, str | None]:\n \"\"\"(clé d'immeuble, numéro d'unité) — adresse d'abord, géo en repli.\"\"\"\n akey, unit = _parse_address(address or \"\", city or \"\")\n if akey:\n return f\"adr:{akey}\", unit\n if coord_key:\n return f\"geo:{coord_key}\", unit\n return None, unit\n\n\ndef _median(vals: list[float]) -> float | None:\n vals = [v for v in vals if v]\n return round(statistics.median(vals)) if vals else None\n\n\ndef _stats_for(members: list[dict], now: float) -> dict:\n \"\"\"Statistiques d'un immeuble à partir de ses annonces (obs. Lou-Ka).\"\"\"\n d30, d90, d365, d730 = (now - 30 * 86400, now - 90 * 86400,\n now - 365 * 86400, now - 730 * 86400)\n actives = [m for m in members if m[\"active\"]]\n first_obs = min(m[\"first_seen\"] for m in members if m[\"first_seen\"])\n last_obs = max(m[\"last_seen\"] for m in members if m[\"last_seen\"])\n obs_days = max(1, round((now - first_obs) / 86400))\n\n units = {m[\"unit\"] for m in members if m[\"unit\"]}\n # unités estimées : numéros d'app distincts observés, plancher = annonces\n # actives en ce moment (elles existent forcément toutes en même temps)\n unites_estimees = max(len(units), len(actives), 1)\n\n n_12m = sum(1 for m in members if (m[\"first_seen\"] or 0) >= d365)\n\n # loyers médians (annonces actives)\n med = _median([m[\"price\"] for m in actives])\n par_cc: dict[str, int] = {}\n by_cc: dict[str, list[float]] = {}\n for m in actives:\n if m[\"price\"] and m[\"bedrooms\"] is not None:\n by_cc.setdefault(str(int(m[\"bedrooms\"])), []).append(m[\"price\"])\n for cc, vals in sorted(by_cc.items()):\n v = _median(vals)\n if v and len(vals) >= 2:\n par_cc[cc] = v\n pi2 = _median([m[\"price\"] / m[\"area\"] * 1.0 for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100])\n pi2 = round(pi2, 2) if pi2 else None\n # _median arrondit à l'entier — refaire proprement pour le $/pi²\n vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]\n pi2 = round(statistics.median(vals_pi2), 2) if vals_pi2 else None\n\n # pression sur les loyers : médiane des PREMIERS prix observés des annonces\n # apparues dans les 12 derniers mois vs celles des 12 mois précédents\n win_new = [m[\"first_price\"] for m in members\n if m[\"first_price\"] and (m[\"first_seen\"] or 0) >= d365]\n win_old = [m[\"first_price\"] for m in members\n if m[\"first_price\"] and d730 <= (m[\"first_seen\"] or 0) < d365]\n pression = None\n if len(win_new) >= MIN_ADS_PRESSURE and len(win_old) >= MIN_ADS_PRESSURE:\n m_new, m_old = statistics.median(win_new), statistics.median(win_old)\n if m_old > 0:\n pression = {\n \"variation_12m\": round((m_new - m_old) / m_old, 4),\n \"mediane_12m\": round(m_new), \"mediane_12_24m\": round(m_old),\n \"n_12m\": len(win_new), \"n_12_24m\": len(win_old),\n \"statut\": \"calculated\",\n }\n\n # score de rotation : annonces des 12 derniers mois / unités estimées\n rotation: dict = {\"statut\": \"donnees_insuffisantes\"}\n if obs_days >= MIN_OBS_DAYS and len(members) >= MIN_ADS_ROTATION:\n ratio = n_12m / unites_estimees\n classe = ROTATION_MAX\n for seuil, nom in ROTATION:\n if ratio < seuil:\n classe = nom\n break\n rotation = {\n \"classe\": classe, \"ratio\": round(ratio, 2),\n \"annonces_12m\": n_12m, \"unites_estimees\": unites_estimees,\n \"observation_jours\": obs_days, \"statut\": \"calculated\",\n \"methode\": (\"annonces observées les 12 derniers mois ÷ unités \"\n \"estimées (numéros d'app distincts, plancher = annonces \"\n \"actives simultanées). Mesure uniquement ce que Lou-Ka \"\n \"observe — pas un taux de roulement démographique.\"),\n }\n\n sources: dict[str, int] = {}\n for m in members:\n sources[m[\"source\"]] = sources.get(m[\"source\"], 0) + 1\n gestionnaires = sorted(s for s in sources\n if s not in PORTALS and s not in CLASSIFIEDS)\n\n return {\n \"annonces_total\": len(members),\n \"annonces_actives\": len(actives),\n \"annonces_30j\": sum(1 for m in members if (m[\"first_seen\"] or 0) >= d30),\n \"annonces_90j\": sum(1 for m in members if (m[\"first_seen\"] or 0) >= d90),\n \"annonces_12m\": n_12m,\n \"unites_identifiees\": len(units),\n \"unites_estimees\": unites_estimees,\n \"loyer_median\": med,\n \"loyer_median_par_cc\": par_cc or None,\n \"pi2_median\": pi2,\n \"pression_loyers\": pression,\n \"rotation\": rotation,\n \"gestionnaires\": gestionnaires,\n \"sources\": sources,\n \"premiere_observation\": first_obs,\n \"derniere_observation\": last_obs,\n \"unites_actives\": sorted(\n [{\"uid\": m[\"uid\"], \"unit_type\": m[\"unit_type\"],\n \"price\": m[\"price\"], \"bedrooms\": m[\"bedrooms\"]}\n for m in actives if not m[\"dup_of\"]],\n key=lambda x: (x[\"price\"] is None, x[\"price\"] or 0))[:24],\n }\n\n\ndef rollup(con=None) -> dict:\n \"\"\"Recalcule building_key de toutes les annonces + la table buildings.\n\n Idempotent, recalcul complet (même philosophie que dedup.run) : les\n annonces INACTIVES participent — l'historique de l'immeuble est justement\n ce qui fait la valeur du passeport.\n \"\"\"\n own = con is None\n if own:\n con = db.connect()\n now = time.time()\n rows = con.execute(\n \"SELECT uid, source, address, city, coord_key, unit_type, bedrooms,\"\n \" price, area_sqft, first_seen, last_seen, active, dup_of, building_key\"\n \" FROM listings\").fetchall()\n\n # premier prix observé par annonce (pression sur les loyers)\n firstp = {r[\"uid\"]: r[\"p\"] for r in con.execute(\n \"SELECT uid, (SELECT price FROM price_log p2 WHERE p2.uid=p1.uid\"\n \" AND p2.price IS NOT NULL ORDER BY ts LIMIT 1) p\"\n \" FROM (SELECT DISTINCT uid FROM price_log) p1\")}\n\n groups: dict[str, list[dict]] = {}\n key_updates: list[tuple[str | None, str]] = []\n for r in rows:\n bkey, unit = building_key(r[\"address\"], r[\"city\"], r[\"coord_key\"])\n if bkey != r[\"building_key\"]:\n key_updates.append((bkey, r[\"uid\"]))\n if not bkey:\n continue\n groups.setdefault(bkey, []).append({\n \"uid\": r[\"uid\"], \"source\": r[\"source\"], \"unit\": unit,\n \"unit_type\": r[\"unit_type\"], \"bedrooms\": r[\"bedrooms\"],\n \"price\": r[\"price\"], \"area\": r[\"area_sqft\"],\n \"first_seen\": r[\"first_seen\"], \"last_seen\": r[\"last_seen\"],\n \"active\": r[\"active\"], \"dup_of\": r[\"dup_of\"],\n \"first_price\": firstp.get(r[\"uid\"]) or r[\"price\"],\n \"address\": r[\"address\"], \"city\": r[\"city\"],\n })\n if key_updates:\n con.executemany(\"UPDATE listings SET building_key=? WHERE uid=?\",\n key_updates)\n\n upserts = []\n for bkey, members in groups.items():\n # doublons inter-sources : un même logement publié sur 3 plateformes ne\n # compte qu'une fois dans les statistiques de l'immeuble\n uniq = [m for m in members if not m[\"dup_of\"]]\n if not uniq:\n continue\n stats = _stats_for(uniq, now)\n # adresse d'affichage : la plus fréquente parmi les membres actifs\n addrs = [m[\"address\"] for m in uniq if m[\"address\"]]\n addr = max(set(addrs), key=addrs.count) if addrs else None\n cities = [m[\"city\"] for m in uniq if m[\"city\"]]\n city = max(set(cities), key=cities.count) if cities else None\n # position : coord de n'importe quel membre géocodé (via coord_key)\n upserts.append((bkey, addr, city,\n json.dumps(stats, ensure_ascii=False), VERSION, now))\n con.executemany(\n \"INSERT INTO buildings (bkey, address, city, stats, version, computed_at)\"\n \" VALUES (?,?,?,?,?,?)\"\n \" ON CONFLICT(bkey) DO UPDATE SET address=excluded.address,\"\n \" city=excluded.city, stats=excluded.stats, version=excluded.version,\"\n \" computed_at=excluded.computed_at\", upserts)\n con.commit()\n out = {\"buildings\": len(upserts), \"listings_scanned\": len(rows),\n \"keys_updated\": len(key_updates)}\n if own:\n con.close()\n return out\n\n\ndef fiche(bkey: str, con=None) -> dict | None:\n \"\"\"Passeport d'un immeuble pour l'API (stats précalculées par rollup).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n row = con.execute(\n \"SELECT bkey, address, city, stats, version, computed_at FROM buildings\"\n \" WHERE bkey=?\", (bkey,)).fetchone()\n if own and row is None:\n con.close()\n return None\n if row is None:\n return None\n out = {\"bkey\": row[\"bkey\"], \"address\": row[\"address\"], \"city\": row[\"city\"],\n \"version\": row[\"version\"], \"computed_at\": row[\"computed_at\"],\n **json.loads(row[\"stats\"] or \"{}\")}\n if own:\n con.close()\n return out\n\n\nif __name__ == \"__main__\":\n print(rollup())\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2780,"cache_read_input_tokens":145021,"cache_creation":{"ephemeral_5m_input_tokens":2780,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6311e3ae-23ac-4686-944d-0b1929b99872"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XHX8DXdDUHV5ZQgDeroExH","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/building.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bad938da-4237-439d-85b9-a24d84084802","timestamp":"2026-08-24T21:55:02.535Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/building.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# building.py : « Passeport de l'immeuble » — entité persistante regroupant les\n# annonces (actives ET historiques) d'un même bâtiment.\n#\n# Clé d'immeuble : la clé d'adresse civique de dedup._parse_address\n# (« civique|rue|ville », préfixe « adr: ») — la même logique éprouvée que la\n# déduplication inter-sources ; repli « geo:coord_key » (~11 m) quand l'adresse\n# n'a pas de numéro civique fiable mais que l'annonce est géocodée.\n#\n# Tout indicateur est calculé UNIQUEMENT sur ce que Lou-Ka observe réellement\n# (annonces collectées par les crawls). Un indicateur sans échantillon\n# suffisant est omis ou marqué « donnees_insuffisantes » — jamais inventé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport statistics\nimport time\n\nfrom . import db\nfrom .dedup import CLASSIFIEDS, PORTALS, _parse_address\n\nVERSION = \"immeuble-1.0\"\n\n# seuils du score de rotation (annonces des 12 derniers mois / unités estimées)\nROTATION = [(0.5, \"faible\"), (1.0, \"normale\"), (2.0, \"élevée\")]\nROTATION_MAX = \"très élevée\"\nMIN_OBS_DAYS = 180 # fenêtre d'observation minimale pour classer la rotation\nMIN_ADS_ROTATION = 3 # nb d'annonces minimal pour classer la rotation\nMIN_ADS_PRESSURE = 3 # nb minimal par fenêtre pour la pression sur les loyers\n\n\ndef building_key(address: str | None, city: str | None,\n coord_key: str | None) -> tuple[str | None, str | None]:\n \"\"\"(clé d'immeuble, numéro d'unité) — adresse d'abord, géo en repli.\"\"\"\n akey, unit = _parse_address(address or \"\", city or \"\")\n if akey:\n return f\"adr:{akey}\", unit\n if coord_key:\n return f\"geo:{coord_key}\", unit\n return None, unit\n\n\ndef _median(vals: list[float]) -> float | None:\n vals = [v for v in vals if v]\n return round(statistics.median(vals)) if vals else None\n\n\ndef _stats_for(members: list[dict], now: float) -> dict:\n \"\"\"Statistiques d'un immeuble à partir de ses annonces (obs. Lou-Ka).\"\"\"\n d30, d90, d365, d730 = (now - 30 * 86400, now - 90 * 86400,\n now - 365 * 86400, now - 730 * 86400)\n actives = [m for m in members if m[\"active\"]]\n first_obs = min(m[\"first_seen\"] for m in members if m[\"first_seen\"])\n last_obs = max(m[\"last_seen\"] for m in members if m[\"last_seen\"])\n obs_days = max(1, round((now - first_obs) / 86400))\n\n units = {m[\"unit\"] for m in members if m[\"unit\"]}\n # unités estimées : numéros d'app distincts observés, plancher = annonces\n # actives en ce moment (elles existent forcément toutes en même temps)\n unites_estimees = max(len(units), len(actives), 1)\n\n n_12m = sum(1 for m in members if (m[\"first_seen\"] or 0) >= d365)\n\n # loyers médians (annonces actives)\n med = _median([m[\"price\"] for m in actives])\n par_cc: dict[str, int] = {}\n by_cc: dict[str, list[float]] = {}\n for m in actives:\n if m[\"price\"] and m[\"bedrooms\"] is not None:\n by_cc.setdefault(str(int(m[\"bedrooms\"])), []).append(m[\"price\"])\n for cc, vals in sorted(by_cc.items()):\n v = _median(vals)\n if v and len(vals) >= 2:\n par_cc[cc] = v\n pi2 = _median([m[\"price\"] / m[\"area\"] * 1.0 for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100])\n pi2 = round(pi2, 2) if pi2 else None\n # _median arrondit à l'entier — refaire proprement pour le $/pi²\n vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]\n pi2 = round(statistics.median(vals_pi2), 2) if vals_pi2 else None\n\n # pression sur les loyers : médiane des PREMIERS prix observés des annonces\n # apparues dans les 12 derniers mois vs celles des 12 mois précédents\n win_new = [m[\"first_price\"] for m in members\n if m[\"first_price\"] and (m[\"first_seen\"] or 0) >= d365]\n win_old = [m[\"first_price\"] for m in members\n if m[\"first_price\"] and d730 <= (m[\"first_seen\"] or 0) < d365]\n pression = None\n if len(win_new) >= MIN_ADS_PRESSURE and len(win_old) >= MIN_ADS_PRESSURE:\n m_new, m_old = statistics.median(win_new), statistics.median(win_old)\n if m_old > 0:\n pression = {\n \"variation_12m\": round((m_new - m_old) / m_old, 4),\n \"mediane_12m\": round(m_new), \"mediane_12_24m\": round(m_old),\n \"n_12m\": len(win_new), \"n_12_24m\": len(win_old),\n \"statut\": \"calculated\",\n }\n\n # score de rotation : annonces des 12 derniers mois / unités estimées\n rotation: dict = {\"statut\": \"donnees_insuffisantes\"}\n if obs_days >= MIN_OBS_DAYS and len(members) >= MIN_ADS_ROTATION:\n ratio = n_12m / unites_estimees\n classe = ROTATION_MAX\n for seuil, nom in ROTATION:\n if ratio < seuil:\n classe = nom\n break\n rotation = {\n \"classe\": classe, \"ratio\": round(ratio, 2),\n \"annonces_12m\": n_12m, \"unites_estimees\": unites_estimees,\n \"observation_jours\": obs_days, \"statut\": \"calculated\",\n \"methode\": (\"annonces observées les 12 derniers mois ÷ unités \"\n \"estimées (numéros d'app distincts, plancher = annonces \"\n \"actives simultanées). Mesure uniquement ce que Lou-Ka \"\n \"observe — pas un taux de roulement démographique.\"),\n }\n\n sources: dict[str, int] = {}\n for m in members:\n sources[m[\"source\"]] = sources.get(m[\"source\"], 0) + 1\n gestionnaires = sorted(s for s in sources\n if s not in PORTALS and s not in CLASSIFIEDS)\n\n return {\n \"annonces_total\": len(members),\n \"annonces_actives\": len(actives),\n \"annonces_30j\": sum(1 for m in members if (m[\"first_seen\"] or 0) >= d30),\n \"annonces_90j\": sum(1 for m in members if (m[\"first_seen\"] or 0) >= d90),\n \"annonces_12m\": n_12m,\n \"unites_identifiees\": len(units),\n \"unites_estimees\": unites_estimees,\n \"loyer_median\": med,\n \"loyer_median_par_cc\": par_cc or None,\n \"pi2_median\": pi2,\n \"pression_loyers\": pression,\n \"rotation\": rotation,\n \"gestionnaires\": gestionnaires,\n \"sources\": sources,\n \"premiere_observation\": first_obs,\n \"derniere_observation\": last_obs,\n \"unites_actives\": sorted(\n [{\"uid\": m[\"uid\"], \"unit_type\": m[\"unit_type\"],\n \"price\": m[\"price\"], \"bedrooms\": m[\"bedrooms\"]}\n for m in actives if not m[\"dup_of\"]],\n key=lambda x: (x[\"price\"] is None, x[\"price\"] or 0))[:24],\n }\n\n\ndef rollup(con=None) -> dict:\n \"\"\"Recalcule building_key de toutes les annonces + la table buildings.\n\n Idempotent, recalcul complet (même philosophie que dedup.run) : les\n annonces INACTIVES participent — l'historique de l'immeuble est justement\n ce qui fait la valeur du passeport.\n \"\"\"\n own = con is None\n if own:\n con = db.connect()\n now = time.time()\n rows = con.execute(\n \"SELECT uid, source, address, city, coord_key, unit_type, bedrooms,\"\n \" price, area_sqft, first_seen, last_seen, active, dup_of, building_key\"\n \" FROM listings\").fetchall()\n\n # premier prix observé par annonce (pression sur les loyers)\n firstp = {r[\"uid\"]: r[\"p\"] for r in con.execute(\n \"SELECT uid, (SELECT price FROM price_log p2 WHERE p2.uid=p1.uid\"\n \" AND p2.price IS NOT NULL ORDER BY ts LIMIT 1) p\"\n \" FROM (SELECT DISTINCT uid FROM price_log) p1\")}\n\n groups: dict[str, list[dict]] = {}\n key_updates: list[tuple[str | None, str]] = []\n for r in rows:\n bkey, unit = building_key(r[\"address\"], r[\"city\"], r[\"coord_key\"])\n if bkey != r[\"building_key\"]:\n key_updates.append((bkey, r[\"uid\"]))\n if not bkey:\n continue\n groups.setdefault(bkey, []).append({\n \"uid\": r[\"uid\"], \"source\": r[\"source\"], \"unit\": unit,\n \"unit_type\": r[\"unit_type\"], \"bedrooms\": r[\"bedrooms\"],\n \"price\": r[\"price\"], \"area\": r[\"area_sqft\"],\n \"first_seen\": r[\"first_seen\"], \"last_seen\": r[\"last_seen\"],\n \"active\": r[\"active\"], \"dup_of\": r[\"dup_of\"],\n \"first_price\": firstp.get(r[\"uid\"]) or r[\"price\"],\n \"address\": r[\"address\"], \"city\": r[\"city\"],\n })\n if key_updates:\n con.executemany(\"UPDATE listings SET building_key=? WHERE uid=?\",\n key_updates)\n\n upserts = []\n for bkey, members in groups.items():\n # doublons inter-sources : un même logement publié sur 3 plateformes ne\n # compte qu'une fois dans les statistiques de l'immeuble\n uniq = [m for m in members if not m[\"dup_of\"]]\n if not uniq:\n continue\n stats = _stats_for(uniq, now)\n # adresse d'affichage : la plus fréquente parmi les membres actifs\n addrs = [m[\"address\"] for m in uniq if m[\"address\"]]\n addr = max(set(addrs), key=addrs.count) if addrs else None\n cities = [m[\"city\"] for m in uniq if m[\"city\"]]\n city = max(set(cities), key=cities.count) if cities else None\n # position : coord de n'importe quel membre géocodé (via coord_key)\n upserts.append((bkey, addr, city,\n json.dumps(stats, ensure_ascii=False), VERSION, now))\n con.executemany(\n \"INSERT INTO buildings (bkey, address, city, stats, version, computed_at)\"\n \" VALUES (?,?,?,?,?,?)\"\n \" ON CONFLICT(bkey) DO UPDATE SET address=excluded.address,\"\n \" city=excluded.city, stats=excluded.stats, version=excluded.version,\"\n \" computed_at=excluded.computed_at\", upserts)\n con.commit()\n out = {\"buildings\": len(upserts), \"listings_scanned\": len(rows),\n \"keys_updated\": len(key_updates)}\n if own:\n con.close()\n return out\n\n\ndef fiche(bkey: str, con=None) -> dict | None:\n \"\"\"Passeport d'un immeuble pour l'API (stats précalculées par rollup).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n row = con.execute(\n \"SELECT bkey, address, city, stats, version, computed_at FROM buildings\"\n \" WHERE bkey=?\", (bkey,)).fetchone()\n if own and row is None:\n con.close()\n return None\n if row is None:\n return None\n out = {\"bkey\": row[\"bkey\"], \"address\": row[\"address\"], \"city\": row[\"city\"],\n \"version\": row[\"version\"], \"computed_at\": row[\"computed_at\"],\n **json.loads(row[\"stats\"] or \"{}\")}\n if own:\n con.close()\n return out\n\n\nif __name__ == \"__main__\":\n print(rollup())\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"7afc6c3e-476e-44a0-9f8c-6fbb811d4f22","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKRcjpToTXvr6NzvTWf","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIShwMKpwEIERgCKkD1d8cwukymhmOTETAT1IjAdxah6kQhc+XMSMqtSs8zMmBLC1g5479D9NP9Ws/wuLB2L8LHiYY4a/xuWUdfCSZnMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG8+7LUBrABAhIM2C7bKO2WFgwY5Z8GGgybqSgvVWoew4UcbbEiMIHUTMFeC5E3nu0XmTdw5Ms+kSIxoaHnDnCNTHZlqWJnD31vYSw2dj3WKK0Ben6gTyqMAW7BXdDSjTL5bYRR7VbNou1PUYOuMUoL8jmBLfvW+NM4jEl9Yj4rUae/oG3DQSmPicWnMsxVs7G90syckiydVcUmio3HiRIdB+2fg+4ubylPDHy3tPBOdP3dOB9PHnGZdvjeGUxyimSDiUm3pP1QTG+IkO7rLEt8vl2QPGaDo8zANt6YJ9AXXFlaFu3rGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6068,"cache_read_input_tokens":147801,"cache_creation":{"ephemeral_5m_input_tokens":6068,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a27bab0f-b7e8-4c24-8f92-45860460a5e1"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKRcjpToTXvr6NzvTWf","type":"message","role":"assistant","content":[{"type":"text","text":"Petit nettoyage d'une redondance dans le calcul du $/pi², puis je passe au module de détection d'annonces recyclées."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6068,"cache_read_input_tokens":147801,"cache_creation":{"ephemeral_5m_input_tokens":6068,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"527562af-b9f3-4000-8b6b-036f05459f4f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKRcjpToTXvr6NzvTWf","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UyxmLNSkZ43PdrGAsVGMXa","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/building.py","old_string":" pi2 = _median([m[\"price\"] / m[\"area\"] * 1.0 for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100])\n pi2 = round(pi2, 2) if pi2 else None\n # _median arrondit à l'entier — refaire proprement pour le $/pi²\n vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]","new_string":" vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6068,"cache_read_input_tokens":147801,"cache_creation":{"ephemeral_5m_input_tokens":6068,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2f10ca22-5c3f-46f6-b77e-8564f9663c7c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UyxmLNSkZ43PdrGAsVGMXa","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/building.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2a2758e1-abf2-4179-adeb-518daf8b243b","timestamp":"2026-08-24T21:55:10.332Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/building.py","oldString":" pi2 = _median([m[\"price\"] / m[\"area\"] * 1.0 for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100])\n pi2 = round(pi2, 2) if pi2 else None\n # _median arrondit à l'entier — refaire proprement pour le $/pi²\n vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]","newString":" vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# building.py : « Passeport de l'immeuble » — entité persistante regroupant les\n# annonces (actives ET historiques) d'un même bâtiment.\n#\n# Clé d'immeuble : la clé d'adresse civique de dedup._parse_address\n# (« civique|rue|ville », préfixe « adr: ») — la même logique éprouvée que la\n# déduplication inter-sources ; repli « geo:coord_key » (~11 m) quand l'adresse\n# n'a pas de numéro civique fiable mais que l'annonce est géocodée.\n#\n# Tout indicateur est calculé UNIQUEMENT sur ce que Lou-Ka observe réellement\n# (annonces collectées par les crawls). Un indicateur sans échantillon\n# suffisant est omis ou marqué « donnees_insuffisantes » — jamais inventé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport statistics\nimport time\n\nfrom . import db\nfrom .dedup import CLASSIFIEDS, PORTALS, _parse_address\n\nVERSION = \"immeuble-1.0\"\n\n# seuils du score de rotation (annonces des 12 derniers mois / unités estimées)\nROTATION = [(0.5, \"faible\"), (1.0, \"normale\"), (2.0, \"élevée\")]\nROTATION_MAX = \"très élevée\"\nMIN_OBS_DAYS = 180 # fenêtre d'observation minimale pour classer la rotation\nMIN_ADS_ROTATION = 3 # nb d'annonces minimal pour classer la rotation\nMIN_ADS_PRESSURE = 3 # nb minimal par fenêtre pour la pression sur les loyers\n\n\ndef building_key(address: str | None, city: str | None,\n coord_key: str | None) -> tuple[str | None, str | None]:\n \"\"\"(clé d'immeuble, numéro d'unité) — adresse d'abord, géo en repli.\"\"\"\n akey, unit = _parse_address(address or \"\", city or \"\")\n if akey:\n return f\"adr:{akey}\", unit\n if coord_key:\n return f\"geo:{coord_key}\", unit\n return None, unit\n\n\ndef _median(vals: list[float]) -> float | None:\n vals = [v for v in vals if v]\n return round(statistics.median(vals)) if vals else None\n\n\ndef _stats_for(members: list[dict], now: float) -> dict:\n \"\"\"Statistiques d'un immeuble à partir de ses annonces (obs. Lou-Ka).\"\"\"\n d30, d90, d365, d730 = (now - 30 * 86400, now - 90 * 86400,\n now - 365 * 86400, now - 730 * 86400)\n actives = [m for m in members if m[\"active\"]]\n first_obs = min(m[\"first_seen\"] for m in members if m[\"first_seen\"])\n last_obs = max(m[\"last_seen\"] for m in members if m[\"last_seen\"])\n obs_days = max(1, round((now - first_obs) / 86400))\n\n units = {m[\"unit\"] for m in members if m[\"unit\"]}\n # unités estimées : numéros d'app distincts observés, plancher = annonces\n # actives en ce moment (elles existent forcément toutes en même temps)\n unites_estimees = max(len(units), len(actives), 1)\n\n n_12m = sum(1 for m in members if (m[\"first_seen\"] or 0) >= d365)\n\n # loyers médians (annonces actives)\n med = _median([m[\"price\"] for m in actives])\n par_cc: dict[str, int] = {}\n by_cc: dict[str, list[float]] = {}\n for m in actives:\n if m[\"price\"] and m[\"bedrooms\"] is not None:\n by_cc.setdefault(str(int(m[\"bedrooms\"])), []).append(m[\"price\"])\n for cc, vals in sorted(by_cc.items()):\n v = _median(vals)\n if v and len(vals) >= 2:\n par_cc[cc] = v\n pi2 = _median([m[\"price\"] / m[\"area\"] * 1.0 for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100])\n pi2 = round(pi2, 2) if pi2 else None\n # _median arrondit à l'entier — refaire proprement pour le $/pi²\n vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives\n if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]\n pi2 = round(statistics.median(vals_pi2), 2) if vals_pi2 else None\n\n # pression sur les loyers : médiane des PREMIERS prix observés des annonces\n # apparues dans les 12 derniers mois vs celles des 12 mois précédents\n win_new = [m[\"first_price\"] for m in members\n if m[\"first_price\"] and (m[\"first_seen\"] or 0) >= d365]\n win_old = [m[\"first_price\"] for m in members\n if m[\"first_price\"] and d730 <= (m[\"first_seen\"] or 0) < d365]\n pression = None\n if len(win_new) >= MIN_ADS_PRESSURE and len(win_old) >= MIN_ADS_PRESSURE:\n m_new, m_old = statistics.median(win_new), statistics.median(win_old)\n if m_old > 0:\n pression = {\n \"variation_12m\": round((m_new - m_old) / m_old, 4),\n \"mediane_12m\": round(m_new), \"mediane_12_24m\": round(m_old),\n \"n_12m\": len(win_new), \"n_12_24m\": len(win_old),\n \"statut\": \"calculated\",\n }\n\n # score de rotation : annonces des 12 derniers mois / unités estimées\n rotation: dict = {\"statut\": \"donnees_insuffisantes\"}\n if obs_days >= MIN_OBS_DAYS and len(members) >= MIN_ADS_ROTATION:\n ratio = n_12m / unites_estimees\n classe = ROTATION_MAX\n for seuil, nom in ROTATION:\n if ratio < seuil:\n classe = nom\n break\n rotation = {\n \"classe\": classe, \"ratio\": round(ratio, 2),\n \"annonces_12m\": n_12m, \"unites_estimees\": unites_estimees,\n \"observation_jours\": obs_days, \"statut\": \"calculated\",\n \"methode\": (\"annonces observées les 12 derniers mois ÷ unités \"\n \"estimées (numéros d'app distincts, plancher = annonces \"\n \"actives simultanées). Mesure uniquement ce que Lou-Ka \"\n \"observe — pas un taux de roulement démographique.\"),\n }\n\n sources: dict[str, int] = {}\n for m in members:\n sources[m[\"source\"]] = sources.get(m[\"source\"], 0) + 1\n gestionnaires = sorted(s for s in sources\n if s not in PORTALS and s not in CLASSIFIEDS)\n\n return {\n \"annonces_total\": len(members),\n \"annonces_actives\": len(actives),\n \"annonces_30j\": sum(1 for m in members if (m[\"first_seen\"] or 0) >= d30),\n \"annonces_90j\": sum(1 for m in members if (m[\"first_seen\"] or 0) >= d90),\n \"annonces_12m\": n_12m,\n \"unites_identifiees\": len(units),\n \"unites_estimees\": unites_estimees,\n \"loyer_median\": med,\n \"loyer_median_par_cc\": par_cc or None,\n \"pi2_median\": pi2,\n \"pression_loyers\": pression,\n \"rotation\": rotation,\n \"gestionnaires\": gestionnaires,\n \"sources\": sources,\n \"premiere_observation\": first_obs,\n \"derniere_observation\": last_obs,\n \"unites_actives\": sorted(\n [{\"uid\": m[\"uid\"], \"unit_type\": m[\"unit_type\"],\n \"price\": m[\"price\"], \"bedrooms\": m[\"bedrooms\"]}\n for m in actives if not m[\"dup_of\"]],\n key=lambda x: (x[\"price\"] is None, x[\"price\"] or 0))[:24],\n }\n\n\ndef rollup(con=None) -> dict:\n \"\"\"Recalcule building_key de toutes les annonces + la table buildings.\n\n Idempotent, recalcul complet (même philosophie que dedup.run) : les\n annonces INACTIVES participent — l'historique de l'immeuble est justement\n ce qui fait la valeur du passeport.\n \"\"\"\n own = con is None\n if own:\n con = db.connect()\n now = time.time()\n rows = con.execute(\n \"SELECT uid, source, address, city, coord_key, unit_type, bedrooms,\"\n \" price, area_sqft, first_seen, last_seen, active, dup_of, building_key\"\n \" FROM listings\").fetchall()\n\n # premier prix observé par annonce (pression sur les loyers)\n firstp = {r[\"uid\"]: r[\"p\"] for r in con.execute(\n \"SELECT uid, (SELECT price FROM price_log p2 WHERE p2.uid=p1.uid\"\n \" AND p2.price IS NOT NULL ORDER BY ts LIMIT 1) p\"\n \" FROM (SELECT DISTINCT uid FROM price_log) p1\")}\n\n groups: dict[str, list[dict]] = {}\n key_updates: list[tuple[str | None, str]] = []\n for r in rows:\n bkey, unit = building_key(r[\"address\"], r[\"city\"], r[\"coord_key\"])\n if bkey != r[\"building_key\"]:\n key_updates.append((bkey, r[\"uid\"]))\n if not bkey:\n continue\n groups.setdefault(bkey, []).append({\n \"uid\": r[\"uid\"], \"source\": r[\"source\"], \"unit\": unit,\n \"unit_type\": r[\"unit_type\"], \"bedrooms\": r[\"bedrooms\"],\n \"price\": r[\"price\"], \"area\": r[\"area_sqft\"],\n \"first_seen\": r[\"first_seen\"], \"last_seen\": r[\"last_seen\"],\n \"active\": r[\"active\"], \"dup_of\": r[\"dup_of\"],\n \"first_price\": firstp.get(r[\"uid\"]) or r[\"price\"],\n \"address\": r[\"address\"], \"city\": r[\"city\"],\n })\n if key_updates:\n con.executemany(\"UPDATE listings SET building_key=? WHERE uid=?\",\n key_updates)\n\n upserts = []\n for bkey, members in groups.items():\n # doublons inter-sources : un même logement publié sur 3 plateformes ne\n # compte qu'une fois dans les statistiques de l'immeuble\n uniq = [m for m in members if not m[\"dup_of\"]]\n if not uniq:\n continue\n stats = _stats_for(uniq, now)\n # adresse d'affichage : la plus fréquente parmi les membres actifs\n addrs = [m[\"address\"] for m in uniq if m[\"address\"]]\n addr = max(set(addrs), key=addrs.count) if addrs else None\n cities = [m[\"city\"] for m in uniq if m[\"city\"]]\n city = max(set(cities), key=cities.count) if cities else None\n # position : coord de n'importe quel membre géocodé (via coord_key)\n upserts.append((bkey, addr, city,\n json.dumps(stats, ensure_ascii=False), VERSION, now))\n con.executemany(\n \"INSERT INTO buildings (bkey, address, city, stats, version, computed_at)\"\n \" VALUES (?,?,?,?,?,?)\"\n \" ON CONFLICT(bkey) DO UPDATE SET address=excluded.address,\"\n \" city=excluded.city, stats=excluded.stats, version=excluded.version,\"\n \" computed_at=excluded.computed_at\", upserts)\n con.commit()\n out = {\"buildings\": len(upserts), \"listings_scanned\": len(rows),\n \"keys_updated\": len(key_updates)}\n if own:\n con.close()\n return out\n\n\ndef fiche(bkey: str, con=None) -> dict | None:\n \"\"\"Passeport d'un immeuble pour l'API (stats précalculées par rollup).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n row = con.execute(\n \"SELECT bkey, address, city, stats, version, computed_at FROM buildings\"\n \" WHERE bkey=?\", (bkey,)).fetchone()\n if own and row is None:\n con.close()\n return None\n if row is None:\n return None\n out = {\"bkey\": row[\"bkey\"], \"address\": row[\"address\"], \"city\": row[\"city\"],\n \"version\": row[\"version\"], \"computed_at\": row[\"computed_at\"],\n **json.loads(row[\"stats\"] or \"{}\")}\n if own:\n con.close()\n return out\n\n\nif __name__ == \"__main__\":\n print(rollup())\n","structuredPatch":[{"oldStart":75,"oldLines":10,"newStart":75,"newLines":6,"lines":[" v = _median(vals)"," if v and len(vals) >= 2:"," par_cc[cc] = v","- pi2 = _median([m[\"price\"] / m[\"area\"] * 1.0 for m in actives","- if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100])","- pi2 = round(pi2, 2) if pi2 else None","- # _median arrondit à l'entier — refaire proprement pour le $/pi²"," vals_pi2 = [m[\"price\"] / m[\"area\"] for m in actives"," if m[\"price\"] and m[\"area\"] and m[\"area\"] > 100]"," pi2 = round(statistics.median(vals_pi2), 2) if vals_pi2 else None"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"2a645d88-b512-4e0c-844c-91230b00c604","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKSAosiwcZv6XJ4Fixk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PXT1w1fZmGUAcZqkWoYRKy","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/recycled.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# recycled.py : détection des annonces RECYCLÉES — le même logement republié\n# (souvent plus cher) après un passage antérieur sur le marché.\n#\n# Méthode : les candidates sont les annonces INACTIVES du même immeuble\n# (building_key — même clé d'adresse que la déduplication). Chaque paire reçoit\n# un score de similarité 0–100 fondé sur des signaux indépendants :\n# numéro d'unité identique, type d'unité, chambres, superficie (±3 %),\n# photos identiques (hash SHA1 du contrôle d'images), texte de description\n# (similarité de Jaccard sur les mots).\n# Seules les correspondances ≥ SEUIL_AFFICHAGE sont retournées, avec les\n# signaux qui les fondent (inférence explicable, jamais de fusion automatique).\n# Cache : table recycled_cache (TTL 7 jours — recalcul léger, borné à\n# l'immeuble).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport re\nimport time\n\nfrom . import db\n\nTTL = 7 * 86400\nSEUIL_AFFICHAGE = 70 # score minimal pour présenter une correspondance\nMAX_CANDIDATES = 200 # garde-fou (tours à très fort volume)\n\n_WORD = re.compile(r\"[a-zà-ÿ0-9]{3,}\")\n\n\ndef _tokens(text: str | None) -> set[str]:\n return set(_WORD.findall((text or \"\").lower()))\n\n\ndef _jaccard(a: set[str], b: set[str]) -> float:\n if not a or not b:\n return 0.0\n return len(a & b) / len(a | b)\n\n\ndef _sha1_set(con, images_json: str | None) -> set[str]:\n try:\n urls = json.loads(images_json or \"[]\")[:20]\n except (ValueError, TypeError):\n return set()\n if not urls:\n return set()\n marks = \",\".join(\"?\" * len(urls))\n return {r[\"sha1\"] for r in con.execute(\n f\"SELECT sha1 FROM image_checks WHERE url IN ({marks})\"\n \" AND sha1 IS NOT NULL\", urls)}\n\n\ndef _unit_of(address: str | None, city: str | None) -> str | None:\n from .dedup import _parse_address\n return _parse_address(address or \"\", city or \"\")[1]\n\n\ndef _score(con, cur: dict, cand: dict) -> tuple[int, list[str]]:\n \"\"\"Score 0–100 + signaux lisibles justifiant la correspondance.\"\"\"\n score = 0\n signals: list[str] = []\n if cur[\"unit\"] and cand[\"unit\"]:\n if cur[\"unit\"] == cand[\"unit\"]:\n score += 35\n signals.append(f\"même numéro d'unité ({cur['unit']})\")\n else:\n return 0, [] # unités connues et différentes : pas le même\n if cur[\"unit_type\"] and cand[\"unit_type\"]:\n if cur[\"unit_type\"] == cand[\"unit_type\"]:\n score += 10\n signals.append(f\"même type ({cur['unit_type']})\")\n else:\n return 0, []\n if cur[\"bedrooms\"] is not None and cand[\"bedrooms\"] is not None:\n if cur[\"bedrooms\"] == cand[\"bedrooms\"]:\n score += 8\n signals.append(\"même nombre de chambres\")\n else:\n return 0, []\n if cur[\"area\"] and cand[\"area\"]:\n if abs(cur[\"area\"] - cand[\"area\"]) <= 0.03 * max(cur[\"area\"], cand[\"area\"]):\n score += 15\n signals.append(f\"même superficie (≈{round(cand['area'])} pi²)\")\n else:\n score -= 10\n # photos identiques (hash du contenu, pas l'URL)\n sh_cur = _sha1_set(con, cur[\"images\"])\n sh_cand = _sha1_set(con, cand[\"images\"])\n if sh_cur and sh_cand:\n overlap = len(sh_cur & sh_cand) / min(len(sh_cur), len(sh_cand))\n if overlap >= 0.5:\n score += 30\n signals.append(f\"{len(sh_cur & sh_cand)} photo(s) identique(s)\")\n elif overlap > 0:\n score += 12\n signals.append(\"photos partiellement identiques\")\n # texte de description\n jac = _jaccard(_tokens(cur[\"description\"]), _tokens(cand[\"description\"]))\n if jac >= 0.7:\n score += 20\n signals.append(\"description quasi identique\")\n elif jac >= 0.45:\n score += 10\n signals.append(\"description très similaire\")\n # même source + même identifiant externe = republication certaine\n if cur[\"source\"] == cand[\"source\"]:\n score += 5\n return min(100, score), signals\n\n\ndef lookup(uid: str, con=None) -> dict:\n \"\"\"Correspondances probables (annonces antérieures du même logement).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n cached = con.execute(\n \"SELECT matches, computed_at FROM recycled_cache WHERE uid=?\",\n (uid,)).fetchone()\n if cached and time.time() - cached[\"computed_at\"] < TTL:\n return json.loads(cached[\"matches\"])\n\n cur = con.execute(\n \"SELECT uid, source, address, city, unit_type, bedrooms, price,\"\n \" area_sqft AS area, description, images, first_seen, building_key,\"\n \" dup_of FROM listings WHERE uid=?\", (uid,)).fetchone()\n out: dict = {\"matches\": [], \"statut\": \"inferred\",\n \"methode\": (\"similarité multi-signaux (unité, type, \"\n \"chambres, superficie, photos par hash, \"\n \"texte) parmi les annonces antérieures du \"\n \"même immeuble\")}\n if cur is None or not cur[\"building_key\"]:\n _store(con, uid, out)\n return out\n cur_d = dict(cur)\n cur_d[\"unit\"] = _unit_of(cur[\"address\"], cur[\"city\"])\n\n # exclure le groupe de doublons inter-sources courant (même annonce)\n dup_group = {uid, cur[\"dup_of\"] or \"\"}\n for r in con.execute(\"SELECT uid FROM listings WHERE dup_of=?\", (uid,)):\n dup_group.add(r[\"uid\"])\n\n cands = con.execute(\n \"SELECT uid, source, address, city, unit_type, bedrooms, price,\"\n \" area_sqft AS area, description, images, first_seen, last_seen\"\n \" FROM listings WHERE building_key=? AND active=0 AND uid<>?\"\n \" ORDER BY last_seen DESC LIMIT ?\",\n (cur[\"building_key\"], uid, MAX_CANDIDATES)).fetchall()\n matches = []\n for c in cands:\n if c[\"uid\"] in dup_group:\n continue\n # une annonce antérieure = terminée avant l'apparition de l'actuelle\n if (c[\"last_seen\"] or 0) > (cur[\"first_seen\"] or 0) + 7 * 86400:\n continue\n cd = dict(c)\n cd[\"unit\"] = _unit_of(c[\"address\"], c[\"city\"])\n score, signals = _score(con, cur_d, cd)\n if score >= SEUIL_AFFICHAGE:\n matches.append({\n \"uid\": c[\"uid\"], \"source\": c[\"source\"],\n \"prix\": c[\"price\"], \"unit_type\": c[\"unit_type\"],\n \"derniere_observation\": c[\"last_seen\"],\n \"premiere_observation\": c[\"first_seen\"],\n \"confiance\": score, \"signaux\": signals,\n })\n matches.sort(key=lambda m: -m[\"confiance\"])\n out[\"matches\"] = matches[:5]\n _store(con, uid, out)\n return out\n finally:\n if own:\n con.close()\n\n\ndef _store(con, uid: str, out: dict) -> None:\n con.execute(\n \"INSERT INTO recycled_cache (uid, matches, computed_at) VALUES (?,?,?)\"\n \" ON CONFLICT(uid) DO UPDATE SET matches=excluded.matches,\"\n \" computed_at=excluded.computed_at\",\n (uid, json.dumps(out, ensure_ascii=False), time.time()))\n con.commit()\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":495,"cache_read_input_tokens":153869,"cache_creation":{"ephemeral_5m_input_tokens":495,"ephemeral_1h_input_tokens":0},"output_tokens":14,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"19487619-35a1-4568-9dca-d3828cc44907"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PXT1w1fZmGUAcZqkWoYRKy","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/recycled.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"de431fc0-e404-4ee3-a954-81b9f5420759","timestamp":"2026-08-24T21:55:43.236Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/recycled.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# recycled.py : détection des annonces RECYCLÉES — le même logement republié\n# (souvent plus cher) après un passage antérieur sur le marché.\n#\n# Méthode : les candidates sont les annonces INACTIVES du même immeuble\n# (building_key — même clé d'adresse que la déduplication). Chaque paire reçoit\n# un score de similarité 0–100 fondé sur des signaux indépendants :\n# numéro d'unité identique, type d'unité, chambres, superficie (±3 %),\n# photos identiques (hash SHA1 du contrôle d'images), texte de description\n# (similarité de Jaccard sur les mots).\n# Seules les correspondances ≥ SEUIL_AFFICHAGE sont retournées, avec les\n# signaux qui les fondent (inférence explicable, jamais de fusion automatique).\n# Cache : table recycled_cache (TTL 7 jours — recalcul léger, borné à\n# l'immeuble).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport re\nimport time\n\nfrom . import db\n\nTTL = 7 * 86400\nSEUIL_AFFICHAGE = 70 # score minimal pour présenter une correspondance\nMAX_CANDIDATES = 200 # garde-fou (tours à très fort volume)\n\n_WORD = re.compile(r\"[a-zà-ÿ0-9]{3,}\")\n\n\ndef _tokens(text: str | None) -> set[str]:\n return set(_WORD.findall((text or \"\").lower()))\n\n\ndef _jaccard(a: set[str], b: set[str]) -> float:\n if not a or not b:\n return 0.0\n return len(a & b) / len(a | b)\n\n\ndef _sha1_set(con, images_json: str | None) -> set[str]:\n try:\n urls = json.loads(images_json or \"[]\")[:20]\n except (ValueError, TypeError):\n return set()\n if not urls:\n return set()\n marks = \",\".join(\"?\" * len(urls))\n return {r[\"sha1\"] for r in con.execute(\n f\"SELECT sha1 FROM image_checks WHERE url IN ({marks})\"\n \" AND sha1 IS NOT NULL\", urls)}\n\n\ndef _unit_of(address: str | None, city: str | None) -> str | None:\n from .dedup import _parse_address\n return _parse_address(address or \"\", city or \"\")[1]\n\n\ndef _score(con, cur: dict, cand: dict) -> tuple[int, list[str]]:\n \"\"\"Score 0–100 + signaux lisibles justifiant la correspondance.\"\"\"\n score = 0\n signals: list[str] = []\n if cur[\"unit\"] and cand[\"unit\"]:\n if cur[\"unit\"] == cand[\"unit\"]:\n score += 35\n signals.append(f\"même numéro d'unité ({cur['unit']})\")\n else:\n return 0, [] # unités connues et différentes : pas le même\n if cur[\"unit_type\"] and cand[\"unit_type\"]:\n if cur[\"unit_type\"] == cand[\"unit_type\"]:\n score += 10\n signals.append(f\"même type ({cur['unit_type']})\")\n else:\n return 0, []\n if cur[\"bedrooms\"] is not None and cand[\"bedrooms\"] is not None:\n if cur[\"bedrooms\"] == cand[\"bedrooms\"]:\n score += 8\n signals.append(\"même nombre de chambres\")\n else:\n return 0, []\n if cur[\"area\"] and cand[\"area\"]:\n if abs(cur[\"area\"] - cand[\"area\"]) <= 0.03 * max(cur[\"area\"], cand[\"area\"]):\n score += 15\n signals.append(f\"même superficie (≈{round(cand['area'])} pi²)\")\n else:\n score -= 10\n # photos identiques (hash du contenu, pas l'URL)\n sh_cur = _sha1_set(con, cur[\"images\"])\n sh_cand = _sha1_set(con, cand[\"images\"])\n if sh_cur and sh_cand:\n overlap = len(sh_cur & sh_cand) / min(len(sh_cur), len(sh_cand))\n if overlap >= 0.5:\n score += 30\n signals.append(f\"{len(sh_cur & sh_cand)} photo(s) identique(s)\")\n elif overlap > 0:\n score += 12\n signals.append(\"photos partiellement identiques\")\n # texte de description\n jac = _jaccard(_tokens(cur[\"description\"]), _tokens(cand[\"description\"]))\n if jac >= 0.7:\n score += 20\n signals.append(\"description quasi identique\")\n elif jac >= 0.45:\n score += 10\n signals.append(\"description très similaire\")\n # même source + même identifiant externe = republication certaine\n if cur[\"source\"] == cand[\"source\"]:\n score += 5\n return min(100, score), signals\n\n\ndef lookup(uid: str, con=None) -> dict:\n \"\"\"Correspondances probables (annonces antérieures du même logement).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n cached = con.execute(\n \"SELECT matches, computed_at FROM recycled_cache WHERE uid=?\",\n (uid,)).fetchone()\n if cached and time.time() - cached[\"computed_at\"] < TTL:\n return json.loads(cached[\"matches\"])\n\n cur = con.execute(\n \"SELECT uid, source, address, city, unit_type, bedrooms, price,\"\n \" area_sqft AS area, description, images, first_seen, building_key,\"\n \" dup_of FROM listings WHERE uid=?\", (uid,)).fetchone()\n out: dict = {\"matches\": [], \"statut\": \"inferred\",\n \"methode\": (\"similarité multi-signaux (unité, type, \"\n \"chambres, superficie, photos par hash, \"\n \"texte) parmi les annonces antérieures du \"\n \"même immeuble\")}\n if cur is None or not cur[\"building_key\"]:\n _store(con, uid, out)\n return out\n cur_d = dict(cur)\n cur_d[\"unit\"] = _unit_of(cur[\"address\"], cur[\"city\"])\n\n # exclure le groupe de doublons inter-sources courant (même annonce)\n dup_group = {uid, cur[\"dup_of\"] or \"\"}\n for r in con.execute(\"SELECT uid FROM listings WHERE dup_of=?\", (uid,)):\n dup_group.add(r[\"uid\"])\n\n cands = con.execute(\n \"SELECT uid, source, address, city, unit_type, bedrooms, price,\"\n \" area_sqft AS area, description, images, first_seen, last_seen\"\n \" FROM listings WHERE building_key=? AND active=0 AND uid<>?\"\n \" ORDER BY last_seen DESC LIMIT ?\",\n (cur[\"building_key\"], uid, MAX_CANDIDATES)).fetchall()\n matches = []\n for c in cands:\n if c[\"uid\"] in dup_group:\n continue\n # une annonce antérieure = terminée avant l'apparition de l'actuelle\n if (c[\"last_seen\"] or 0) > (cur[\"first_seen\"] or 0) + 7 * 86400:\n continue\n cd = dict(c)\n cd[\"unit\"] = _unit_of(c[\"address\"], c[\"city\"])\n score, signals = _score(con, cur_d, cd)\n if score >= SEUIL_AFFICHAGE:\n matches.append({\n \"uid\": c[\"uid\"], \"source\": c[\"source\"],\n \"prix\": c[\"price\"], \"unit_type\": c[\"unit_type\"],\n \"derniere_observation\": c[\"last_seen\"],\n \"premiere_observation\": c[\"first_seen\"],\n \"confiance\": score, \"signaux\": signals,\n })\n matches.sort(key=lambda m: -m[\"confiance\"])\n out[\"matches\"] = matches[:5]\n _store(con, uid, out)\n return out\n finally:\n if own:\n con.close()\n\n\ndef _store(con, uid: str, out: dict) -> None:\n con.execute(\n \"INSERT INTO recycled_cache (uid, matches, computed_at) VALUES (?,?,?)\"\n \" ON CONFLICT(uid) DO UPDATE SET matches=excluded.matches,\"\n \" computed_at=excluded.computed_at\",\n (uid, json.dumps(out, ensure_ascii=False), time.time()))\n con.commit()\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"84691f07-55db-4f65-a89c-4ce2bbd1cd6d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKUbdRe4ttuE97thxPs","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS3wMKpwEIERgCKkA8O9VuNYcYsrdEg6YBuo2FShtIuwISo+FNwzXXbvKkl90ffBJVPX3790hf3BabvEaZ423Kz+nD4DrpVd423q7XMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHk+7LUBrABAhIMCsml8tj+Q2wImqXrGgxgzNIEtqcnk43N6egiMCiG/izhxRcVHIPKcG1CkTuzVSVxZdiFP9IpZp08ORtRwcHq3s7yWlxdo97dz9Pu5yrkAULdQRh33E6uN0F5xX//0X5+ObnBTTsVHf9lUp1OPu9Yr4r7lOdV6WJrGmfaYb81jC666NI0KMqx//MgOZHBGIiB3/Yx9cI2lefXycsr5z04vG3+TcaHfxCe5+QY3okBHuunLcvKZClKpuBBTofhwsocAW/2sGJNalUE70yOqTqf0LdCG/egUDzJ32TaXmzAgjLruJWd+qYm3Cm6Y0fh1nc6n3adBdiPtEX3IeLtIfmh8XyIPETM9x1w0PR05gnL8XAAsxVi0MFoIT0durX7uRcdKQ7Hv7Tfh/0Akh7uI0Du+cSWqBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3081,"cache_read_input_tokens":154364,"cache_creation":{"ephemeral_5m_input_tokens":3081,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9c673a59-b18b-486b-8d91-099379abb617"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKUbdRe4ttuE97thxPs","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS2AMKqAEIERgCKkBnu/kjqyM7CTNiukT0emDqw8Jlg1lQOZ34YzAtVoSC3To/3lrC0iiIQHD1AKm8Zxrs0iTNIE7Hq9bfxS6gy0OGMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagB5Puy1AawAQISDOsDtzHnl8fzaGeWMhoMzc8XLMPMDMrNTYqjIjCEWD9wFf+1sLSsBACHPYjQiztB9Icmk1anqYUl0/mgBAvckqDfFLvV96CE5cYJVJwq3AEJ/K+u65T5EbRfJekhzoUX7vHHqJuDeIkXJkTNZdoKWt94x6J91FZUcb5q7vsw9Kl/VtNtvtAfS7CkzRD5zOfV79q/+lg4jbDGN03vGoFxV1xCjkJN/WF2iYnSe2FHF9VztFP3wqOdj4H20d2r4WHxv6nNeWCWPBbAHY27ruWNAy2OxDxn8PgKEGuEALtn+PqSZjqzPUYDIWFg3z0U+F6FnZ4CbIJOMLVyR4BhlGlH3daUVcqoYJ4HWWLOHlGoufv3XVZZ4kQdO0wuqy2tjYjDNaPrIdvtv895MaPJGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3081,"cache_read_input_tokens":154364,"cache_creation":{"ephemeral_5m_input_tokens":3081,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"eacf782a-1a89-4e0d-9568-b002886674b2"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKUbdRe4ttuE97thxPs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MaHsYaNvZcoGjpwNFwHxgJ","name":"Bash","input":{"command":"cd ~/apps/lou-ka && sed -n '100,240p' louka/hydro.py && grep -n \"inclusions\\|parking\" louka/normalize.py | head -30","description":"Format de retour hydro.estimate et structure des inclusions"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3081,"cache_read_input_tokens":154364,"cache_creation":{"ephemeral_5m_input_tokens":3081,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"847e20f7-8796-4df2-9570-63c796676180"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MaHsYaNvZcoGjpwNFwHxgJ","type":"tool_result","content":"def _capsolver(key: str) -> str | None:\n r = urllib.request.urlopen(urllib.request.Request(\n \"https://api.capsolver.com/createTask\",\n data=json.dumps({\"clientKey\": key, \"task\": {\n \"type\": \"ReCaptchaV2TaskProxyLess\",\n \"websiteURL\": PAGE_URL, \"websiteKey\": SITE_KEY}}).encode(),\n headers={\"Content-Type\": \"application/json\"}), timeout=30)\n tid = json.load(r).get(\"taskId\")\n if not tid:\n return None\n for _ in range(24):\n time.sleep(5)\n res = json.load(urllib.request.urlopen(urllib.request.Request(\n \"https://api.capsolver.com/getTaskResult\",\n data=json.dumps({\"clientKey\": key, \"taskId\": tid}).encode(),\n headers={\"Content-Type\": \"application/json\"}), timeout=30))\n if res.get(\"status\") == \"ready\":\n return res[\"solution\"][\"gRecaptchaResponse\"]\n if res.get(\"status\") != \"processing\":\n return None\n return None\n\n\ndef _post(opener, url: str, payload: dict) -> dict | None:\n req = urllib.request.Request(url, data=json.dumps(payload).encode(),\n headers={\"Content-Type\": \"application/json\",\n \"User-Agent\": UA,\n \"Origin\": \"https://session.hydroquebec.com\",\n \"Referer\": PAGE_URL})\n try:\n with opener.open(req, timeout=60) as r:\n return json.load(r)\n except Exception:\n return None\n\n\n# --- cache --------------------------------------------------------------------\ndef _connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH, timeout=15)\n con.row_factory = sqlite3.Row\n con.execute(\"\"\"CREATE TABLE IF NOT EXISTS hydro_cache (\n cle TEXT PRIMARY KEY, adresse TEXT, cout_annuel REAL,\n cout_mensuel REAL, payload TEXT, fetched_at REAL)\"\"\")\n cols = {r[1] for r in con.execute(\"PRAGMA table_info(hydro_cache)\")}\n if \"uid\" not in cols:\n con.execute(\"ALTER TABLE hydro_cache ADD COLUMN uid TEXT\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_hydro_uid \"\n \"ON hydro_cache(uid)\")\n if \"kwh\" not in cols:\n con.execute(\"ALTER TABLE hydro_cache ADD COLUMN kwh INTEGER\")\n return con\n\n\ndef _mark_negatif(con: sqlite3.Connection, cle: str, uid: str | None) -> None:\n \"\"\"Échec (adresse inconnue d'HQ) : ligne sans coût, pour ne pas re-résoudre\n de captcha avant NEG_TTL. N'écrase jamais un succès existant.\"\"\"\n with con:\n con.execute(\n \"INSERT OR IGNORE INTO hydro_cache \"\n \"(cle, adresse, cout_annuel, cout_mensuel, payload, fetched_at, \"\n \"uid, kwh) VALUES (?,?,NULL,NULL,NULL,?,?,NULL)\",\n (cle, \"\", time.time(), uid))\n con.execute(\n \"UPDATE hydro_cache SET fetched_at=?, uid=COALESCE(?, uid) \"\n \"WHERE cle=? AND cout_annuel IS NULL\",\n (time.time(), uid, cle))\n\n\ndef _civic(adresse: str) -> str | None:\n m = re.match(r\"\\s*(\\d+)([A-Za-z]{0,3})\\b\", adresse or \"\")\n if not m:\n return None\n num, suf = m.group(1), m.group(2)\n reste = (adresse or \"\")[m.end():].lstrip(\" ,\")\n # rues ordinales : « 15e Avenue », « 1re Avenue », « 2e Rang » — le suffixe\n # ordinal colle au chiffre et est suivi d'un type de voie\n if (suf.lower() in (\"e\", \"re\", \"er\", \"eme\", \"ieme\", \"ième\", \"nd\", \"nde\")\n and re.match(r\"(av|ave|avenue|rue|rang|boul|ch|chemin)\\b\",\n reste, re.I)):\n return None\n return num + suf # numéro civique, avec lettre d'appartement (24A)\n\n\ndef _parse_addr(adresse: str) -> tuple[str, str] | None:\n \"\"\"Extrait (numéro civique, code postal) d'une adresse de fiche.\"\"\"\n cp = re.search(r\"([A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d)\", adresse or \"\")\n civ = _civic(adresse or \"\")\n if not cp or not civ:\n return None\n return civ, cp.group(1).upper().replace(\" \", \"\")\n\n\n_MAPBOX_TOKEN: list[str] = []\n\n\ndef _mapbox_token() -> str | None:\n if not _MAPBOX_TOKEN:\n cfg = (Path(__file__).resolve().parent.parent / \"frontend\" / \"src\"\n / \"kamaps\" / \"config.ts\")\n if cfg.exists():\n m = re.search(r'\"(pk\\.[A-Za-z0-9._-]+)\"', cfg.read_text())\n _MAPBOX_TOKEN.append(m.group(1) if m else \"\")\n else:\n _MAPBOX_TOKEN.append(\"\")\n return _MAPBOX_TOKEN[0] or None\n\n\ndef _postal_from_latlng(lat: float, lng: float) -> str | None:\n \"\"\"Code postal via géocodage inverse Mapbox (adresses sans CP).\"\"\"\n tok = _mapbox_token()\n if not tok:\n return None\n url = (f\"https://api.mapbox.com/geocoding/v5/mapbox.places/{lng},{lat}.json\"\n f\"?types=postcode&country=CA&access_token={tok}\")\n try:\n with urllib.request.urlopen(url, timeout=15) as r:\n feats = json.load(r).get(\"features\") or []\n if feats:\n return (feats[0].get(\"text\") or \"\").upper().replace(\" \", \"\")\n except Exception:\n return None\n return None\n\n\ndef _resolve_postal(civic: str, adresse: str,\n lat: float | None, lng: float | None) -> str | None:\n \"\"\"Code postal le plus PRÉCIS pour civic+adresse. Priorité au géocodage\n AVANT de l'adresse complète (rooftop) ancré par proximity=lat/lng — plus\n fiable que l'inverse du lat/lng souvent approximatif des annonces. Vérifie\n que le numéro civique du résultat correspond ; sinon repli sur l'inverse.\"\"\"\n tok = _mapbox_token()\n if tok and adresse:\n try:\n q = urllib.parse.quote(adresse)\n url = (f\"https://api.mapbox.com/geocoding/v5/mapbox.places/{q}.json\"\n f\"?country=CA&types=address&limit=1&language=fr\")\n if lat is not None and lng is not None:\n url += f\"&proximity={lng},{lat}\"\n url += f\"&access_token={tok}\"\n with urllib.request.urlopen(url, timeout=15) as r:\n423: # inclusions\n452: \"parking_kw\": (r\"stationnement|parking|garage\", None),\n481:_PARKING_INT_RE = re.compile(r\"(?:stationnement|garage|parking)[^.]{0,30}(?:interieur|souterrain|sous[- ]terrain|indoor)|garage\")\n482:_PARKING_EXT_RE = re.compile(r\"(?:stationnement|parking)[^.]{0,30}(?:exterieur|outdoor)\")\n483:_PARKING_INCL_RE = re.compile(r\"(?:stationnement|parking)[^.]{0,30}inclus\")\n485: r\"(?:stationnement|parking)[^.]{0,60}?(?:en sus|en option|disponible)\")\n487: r\"(?:stationnement|parking)[^.]{0,60}?\"\n504: r\"|stationnement|parking)[^|.]{0,25}sur demande[^|.]*\"\n507: r\"|[^|.]*bike (?:parking|storage|racks?)[^|.]*\"\n512: # de promo ne décrit pas les inclusions du logement\n530: Clés : inclusions{}, appliances{}, pets, smoking, furnished, parking{},\n554: inclusions = {k: v for k, v in flags.items()\n557: inclusions.update({\"heating\": False, \"electricity\": False, \"hot_water\": False})\n558: if inclusions:\n559: details[\"inclusions\"] = inclusions\n597: if flags.get(\"parking_kw\"):\n598: parking: dict = {\"available\": True}\n602: parking[\"type\"] = \"intérieur et extérieur\"\n604: parking[\"type\"] = \"intérieur\"\n606: parking[\"type\"] = \"extérieur\"\n608: parking[\"included\"] = True\n610: parking[\"included\"] = False\n615: parking[\"price\"] = prix\n616: details[\"parking\"] = parking\n647: (inclusions, appliances, parking, contact) sont fusionnés clé par clé.","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"74e832fd-3745-4698-a207-f7b0f89b0980","timestamp":"2026-08-24T21:55:49.933Z","tool_use_result":{"stdout":"def _capsolver(key: str) -> str | None:\n r = urllib.request.urlopen(urllib.request.Request(\n \"https://api.capsolver.com/createTask\",\n data=json.dumps({\"clientKey\": key, \"task\": {\n \"type\": \"ReCaptchaV2TaskProxyLess\",\n \"websiteURL\": PAGE_URL, \"websiteKey\": SITE_KEY}}).encode(),\n headers={\"Content-Type\": \"application/json\"}), timeout=30)\n tid = json.load(r).get(\"taskId\")\n if not tid:\n return None\n for _ in range(24):\n time.sleep(5)\n res = json.load(urllib.request.urlopen(urllib.request.Request(\n \"https://api.capsolver.com/getTaskResult\",\n data=json.dumps({\"clientKey\": key, \"taskId\": tid}).encode(),\n headers={\"Content-Type\": \"application/json\"}), timeout=30))\n if res.get(\"status\") == \"ready\":\n return res[\"solution\"][\"gRecaptchaResponse\"]\n if res.get(\"status\") != \"processing\":\n return None\n return None\n\n\ndef _post(opener, url: str, payload: dict) -> dict | None:\n req = urllib.request.Request(url, data=json.dumps(payload).encode(),\n headers={\"Content-Type\": \"application/json\",\n \"User-Agent\": UA,\n \"Origin\": \"https://session.hydroquebec.com\",\n \"Referer\": PAGE_URL})\n try:\n with opener.open(req, timeout=60) as r:\n return json.load(r)\n except Exception:\n return None\n\n\n# --- cache --------------------------------------------------------------------\ndef _connect() -> sqlite3.Connection:\n DB_PATH.parent.mkdir(parents=True, exist_ok=True)\n con = sqlite3.connect(DB_PATH, timeout=15)\n con.row_factory = sqlite3.Row\n con.execute(\"\"\"CREATE TABLE IF NOT EXISTS hydro_cache (\n cle TEXT PRIMARY KEY, adresse TEXT, cout_annuel REAL,\n cout_mensuel REAL, payload TEXT, fetched_at REAL)\"\"\")\n cols = {r[1] for r in con.execute(\"PRAGMA table_info(hydro_cache)\")}\n if \"uid\" not in cols:\n con.execute(\"ALTER TABLE hydro_cache ADD COLUMN uid TEXT\")\n con.execute(\"CREATE INDEX IF NOT EXISTS idx_hydro_uid \"\n \"ON hydro_cache(uid)\")\n if \"kwh\" not in cols:\n con.execute(\"ALTER TABLE hydro_cache ADD COLUMN kwh INTEGER\")\n return con\n\n\ndef _mark_negatif(con: sqlite3.Connection, cle: str, uid: str | None) -> None:\n \"\"\"Échec (adresse inconnue d'HQ) : ligne sans coût, pour ne pas re-résoudre\n de captcha avant NEG_TTL. N'écrase jamais un succès existant.\"\"\"\n with con:\n con.execute(\n \"INSERT OR IGNORE INTO hydro_cache \"\n \"(cle, adresse, cout_annuel, cout_mensuel, payload, fetched_at, \"\n \"uid, kwh) VALUES (?,?,NULL,NULL,NULL,?,?,NULL)\",\n (cle, \"\", time.time(), uid))\n con.execute(\n \"UPDATE hydro_cache SET fetched_at=?, uid=COALESCE(?, uid) \"\n \"WHERE cle=? AND cout_annuel IS NULL\",\n (time.time(), uid, cle))\n\n\ndef _civic(adresse: str) -> str | None:\n m = re.match(r\"\\s*(\\d+)([A-Za-z]{0,3})\\b\", adresse or \"\")\n if not m:\n return None\n num, suf = m.group(1), m.group(2)\n reste = (adresse or \"\")[m.end():].lstrip(\" ,\")\n # rues ordinales : « 15e Avenue », « 1re Avenue », « 2e Rang » — le suffixe\n # ordinal colle au chiffre et est suivi d'un type de voie\n if (suf.lower() in (\"e\", \"re\", \"er\", \"eme\", \"ieme\", \"ième\", \"nd\", \"nde\")\n and re.match(r\"(av|ave|avenue|rue|rang|boul|ch|chemin)\\b\",\n reste, re.I)):\n return None\n return num + suf # numéro civique, avec lettre d'appartement (24A)\n\n\ndef _parse_addr(adresse: str) -> tuple[str, str] | None:\n \"\"\"Extrait (numéro civique, code postal) d'une adresse de fiche.\"\"\"\n cp = re.search(r\"([A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d)\", adresse or \"\")\n civ = _civic(adresse or \"\")\n if not cp or not civ:\n return None\n return civ, cp.group(1).upper().replace(\" \", \"\")\n\n\n_MAPBOX_TOKEN: list[str] = []\n\n\ndef _mapbox_token() -> str | None:\n if not _MAPBOX_TOKEN:\n cfg = (Path(__file__).resolve().parent.parent / \"frontend\" / \"src\"\n / \"kamaps\" / \"config.ts\")\n if cfg.exists():\n m = re.search(r'\"(pk\\.[A-Za-z0-9._-]+)\"', cfg.read_text())\n _MAPBOX_TOKEN.append(m.group(1) if m else \"\")\n else:\n _MAPBOX_TOKEN.append(\"\")\n return _MAPBOX_TOKEN[0] or None\n\n\ndef _postal_from_latlng(lat: float, lng: float) -> str | None:\n \"\"\"Code postal via géocodage inverse Mapbox (adresses sans CP).\"\"\"\n tok = _mapbox_token()\n if not tok:\n return None\n url = (f\"https://api.mapbox.com/geocoding/v5/mapbox.places/{lng},{lat}.json\"\n f\"?types=postcode&country=CA&access_token={tok}\")\n try:\n with urllib.request.urlopen(url, timeout=15) as r:\n feats = json.load(r).get(\"features\") or []\n if feats:\n return (feats[0].get(\"text\") or \"\").upper().replace(\" \", \"\")\n except Exception:\n return None\n return None\n\n\ndef _resolve_postal(civic: str, adresse: str,\n lat: float | None, lng: float | None) -> str | None:\n \"\"\"Code postal le plus PRÉCIS pour civic+adresse. Priorité au géocodage\n AVANT de l'adresse complète (rooftop) ancré par proximity=lat/lng — plus\n fiable que l'inverse du lat/lng souvent approximatif des annonces. Vérifie\n que le numéro civique du résultat correspond ; sinon repli sur l'inverse.\"\"\"\n tok = _mapbox_token()\n if tok and adresse:\n try:\n q = urllib.parse.quote(adresse)\n url = (f\"https://api.mapbox.com/geocoding/v5/mapbox.places/{q}.json\"\n f\"?country=CA&types=address&limit=1&language=fr\")\n if lat is not None and lng is not None:\n url += f\"&proximity={lng},{lat}\"\n url += f\"&access_token={tok}\"\n with urllib.request.urlopen(url, timeout=15) as r:\n423: # inclusions\n452: \"parking_kw\": (r\"stationnement|parking|garage\", None),\n481:_PARKING_INT_RE = re.compile(r\"(?:stationnement|garage|parking)[^.]{0,30}(?:interieur|souterrain|sous[- ]terrain|indoor)|garage\")\n482:_PARKING_EXT_RE = re.compile(r\"(?:stationnement|parking)[^.]{0,30}(?:exterieur|outdoor)\")\n483:_PARKING_INCL_RE = re.compile(r\"(?:stationnement|parking)[^.]{0,30}inclus\")\n485: r\"(?:stationnement|parking)[^.]{0,60}?(?:en sus|en option|disponible)\")\n487: r\"(?:stationnement|parking)[^.]{0,60}?\"\n504: r\"|stationnement|parking)[^|.]{0,25}sur demande[^|.]*\"\n507: r\"|[^|.]*bike (?:parking|storage|racks?)[^|.]*\"\n512: # de promo ne décrit pas les inclusions du logement\n530: Clés : inclusions{}, appliances{}, pets, smoking, furnished, parking{},\n554: inclusions = {k: v for k, v in flags.items()\n557: inclusions.update({\"heating\": False, \"electricity\": False, \"hot_water\": False})\n558: if inclusions:\n559: details[\"inclusions\"] = inclusions\n597: if flags.get(\"parking_kw\"):\n598: parking: dict = {\"available\": True}\n602: parking[\"type\"] = \"intérieur et extérieur\"\n604: parking[\"type\"] = \"intérieur\"\n606: parking[\"type\"] = \"extérieur\"\n608: parking[\"included\"] = True\n610: parking[\"included\"] = False\n615: parking[\"price\"] = prix\n616: details[\"parking\"] = parking\n647: (inclusions, appliances, parking, contact) sont fusionnés clé par clé.","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"4bcd0f21-510f-49ab-ab27-777ed9f11d40","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKV6D99iB274mvEwccU","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISvAQKpwEIERgCKkDzj+ilUdYdejFmQ00n+DdmkB5bNQYT55/h56hyoMrx1quIMFta34KuWjdoLk5TKxhQpN7NLGLXdBaG4xB/oiD+Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHr+7LUBrABAhIMAG5FJ5KCLHiqeW5BGgxmX5UtJ5UcYkHXavUiMI+nA5lMBICMOTKoD9eSQl06Ys6Mzr64BX9rAsdEarE4XY/KZ1mK9zKCRDNmiqdbNirBAqdyC9kT3OHdx8EKZX/dv0Gct0oA0U7AOCfvXAeXmfecgNIcXjWhJ2/ufULn4IVKC9XvOwfvhytAIKQn5VgK7uQ1dMxw86hKWO0PexL+qPC2iToZUDn5dozZbPFIzI3DNeyV5cfTLjTeIO9O9yMTDnvimuqIMnssXhQ6I6cgXBlzZNAzFk/5BU4sCfDqLI7pkXpcB4M4XkYMiIYvcMrGdP8J4rL9kyqnLBS+Ye+jzr2OXsy01PhW+HCzm8O1GYHiPN7LEIBLv9QPqNe0xVZ6j/m1g3FKfla2mlpj7e190LZqpFkqDfGnx1HNyJNIqnei4p2JGMYv13miAeyb1Vj2gane0pTQ5uu4D0oTIp45lzqceeEHO0y7B7AIoUo8I6JnhFcl41b3ZR/AbTYlbNdc50I+1ThU8eHfMe8p4cvdWaUKNxgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3567,"cache_read_input_tokens":157445,"cache_creation":{"ephemeral_5m_input_tokens":3567,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"977c13fd-a687-421d-9a9c-cd1a683fcfad"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKV6D99iB274mvEwccU","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DxQioSJmyq191hSdaDhoqT","name":"Bash","input":{"command":"cd ~/apps/lou-ka && sed -n '415,470p' louka/normalize.py && grep -n \"def estimate\" louka/hydro.py && sed -n \"$(grep -n 'def estimate' louka/hydro.py | cut -d: -f1),+40p\" louka/hydro.py","description":"Clés d'inclusions et signature de hydro.estimate"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3567,"cache_read_input_tokens":157445,"cache_creation":{"ephemeral_5m_input_tokens":3567,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"36149601-9b26-4791-9bba-3fe8a62cb300"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DxQioSJmyq191hSdaDhoqT","type":"tool_result","content":"# ---------------------------------------------------------------------------\n# Commodités -> tags canoniques (details JSON)\n# ---------------------------------------------------------------------------\n# Chaque règle : clé canonique -> (motifs positifs, motifs négatifs).\n# Les motifs sont matchés sur du texte sans accents en minuscules.\n# Absence de match = inconnu (la clé n'apparaît pas), jamais « false » deviné.\n\n_AMENITY_RULES: dict[str, tuple[str, str | None]] = {\n # inclusions\n \"heating\": (r\"chauffage|chauffe[e]? |chauffe[e]?$|heating|heat included|\\bheat\\b\",\n r\"chauffage non inclus|sans chauffage|non[- ]chauffe|pas chauffe|heat pump\"\n r\"|chauffage[^|.]{0,60}charge d(?:u|es) locataires?\"),\n \"electricity\": (r\"electricite|hydro inclus|electricity|eclaire\\b|\\bhydro\\b\",\n r\"electricite non incluse?|non[- ]eclaire\"\n r\"|electricite[^|.]{0,60}charge d(?:u|es) locataires?\"),\n \"hot_water\": (r\"eau chaude|hot water\",\n r\"eau chaude non incluse?|eau chaude[^|.]{0,60}charge d(?:u|es) locataires?\"),\n \"internet\": (r\"internet|wi[- ]?fi\", r\"internet non inclus\"),\n \"cable\": (r\"\\bcable\\b|television|telus|videotron|bell fibe\", None),\n # électroménagers\n \"fridge\": (r\"refrigerateur|refrigerator|\\bfrigo\\b|\\bfridge\\b|electromenagers?( inclus)?|appliances\", None),\n \"stove\": (r\"cuisiniere|\\bstove\\b|\\bfour\\b|plaques? de cuisson\", None),\n \"dishwasher\": (r\"lave[- ]vaisselle|dishwasher\", r\"entrees? (?:pour |de )?lave[- ]vaisselle|dishwasher hookup\"),\n \"dishwasher_hookup\": (r\"entrees? (?:pour |de )?lave[- ]vaisselle|dishwasher hookups?\", None),\n \"washer_dryer\": (r\"laveuse[- /]secheuse|laveuse et secheuse|\\bwasher\\b|\\bdryer\\b\",\n r\"entrees?[^|.]{0,35}laveuse|washer.{0,10}hookup\"),\n \"washer_dryer_hookup\": (r\"entrees?[^|.]{0,35}laveuse|sorties? laveuse|washer.{0,10}hookups?\", None),\n \"ac\": (r\"climatis|air climatise|\\ba/?c\\b|air conditioning|central air|cooling\\s*:|thermopompe\", None),\n # immeuble\n \"elevator\": (r\"ascen[cs]eur|elevator\", None),\n \"balcony\": (r\"balcon|terrasse|terrace|patio|loggia\",\n r\"pas de balcon|sans balcon|no balcony|aucun balcon\"),\n \"pool\": (r\"piscine|\\bpool\\b|\\bspa\\b\", None),\n \"gym\": (r\"\\bgym\\b|salle d.?entrainement|entrainement|fitness|salle de sport\", None),\n \"laundry\": (r\"buanderie|salle de lavage|laundry (?:room|facilit)\", None),\n \"storage\": (r\"rangement|\\blocker\\b|espace de rangement|storage\", None),\n \"furnished_kw\": (r\"\\bmeuble\\b|\\bmeublee?s?\\b|furnished\", r\"non[- ]meuble|unfurnished\"),\n \"parking_kw\": (r\"stationnement|parking|garage\", None),\n}\n\n_PETS_YES_RE = re.compile(\n r\"animaux (?:de compagnie )?(?:acceptes|admis|bienvenus|permis|autorises|ok)\"\n r\"|pet[s]?[- ]friendly|pets? (?:allowed|welcome)\"\n r\"|chats? (?:et chiens? )?(?:acceptes?|admis)\"\n r\"|chiens? (?:et chats? )?(?:permis|acceptes?|admis)\"\n r\"|anima(?:l|ux)[^|.]{0,15}(?:bienvenus?|acceptes?|admis|permis)\")\n# les deux espèces explicitement acceptées : « oui » sans ambiguïté,\n# testé AVANT les motifs « conditions » (sinon « chats acceptés » l'emporte)\n_PETS_BOTH_RE = re.compile(\n r\"(?:chiens? et chats?|chats? et chiens?) (?:acceptes|admis|bienvenus|permis|autorises)\")\n_PETS_NO_RE = re.compile(\n r\"(?:pas d.?animaux|aucun animal|animaux (?:refuses|interdits|non admis|non acceptes|non autorises|non permis))|no pets?\"\n r\"|animaux (?:permis|acceptes|admis|autorises)?\\s*:\\s*non\"\n r\"|animaux[^|.]{0,15}interdits\")\n_PETS_COND_RE = re.compile(\n r\"animaux (?:sous conditions?|sur approbation)|petits? animaux|petits? chiens?\"\n269:def estimate(civic: str | None = None, postal: str | None = None,\ndef estimate(civic: str | None = None, postal: str | None = None,\n adresse: str | None = None, lat: float | None = None,\n lng: float | None = None, uid: str | None = None,\n solve: bool = True) -> dict:\n \"\"\"Coût d'électricité annuel estimé à une adresse (cache 6 mois).\n\n `solve=False` : ne consulte QUE le cache (aucune résolution de captcha,\n aucun coût) — utilisé pour l'affichage automatique des fiches.\n Le code postal est extrait de l'adresse, ou dérivé du couple lat/lng par\n géocodage inverse Mapbox si l'adresse n'en contient pas (cas d'Immo-Ka).\n Le cache est indexé par `uid` (clé stable de l'annonce) ET par\n civique|code postal, pour que l'affichage cache-only fonctionne même\n quand l'adresse ne porte pas de code postal.\n \"\"\"\n # lecture cache par uid d'abord (ne nécessite ni CP ni Mapbox)\n if uid:\n con = _connect()\n row = con.execute(\n \"SELECT * FROM hydro_cache WHERE uid=? AND cout_annuel IS NOT NULL \"\n \"AND fetched_at>?\", (uid, time.time() - TTL)).fetchone()\n con.close()\n if row:\n return {\"disponible\": True, \"adresse\": row[\"adresse\"],\n \"cout_annuel\": row[\"cout_annuel\"],\n \"cout_mensuel\": row[\"cout_mensuel\"],\n \"kwh_annuel\": row[\"kwh\"], \"cache\": True}\n\n if not civic and adresse:\n civic = _civic(adresse)\n if not postal and adresse:\n cp = re.search(r\"([A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d)\", adresse)\n if cp:\n postal = cp.group(1)\n # cache-only : ne pas déclencher Mapbox ; proposer le bouton si un calcul\n # est faisable (civique connu + CP OU coordonnées disponibles)\n if not solve:\n faisable = bool(civic) and (bool(postal) or\n (lat is not None and lng is not None))\n return {\"disponible\": False,\n \"raison\": \"non calculé\" if faisable else \"adresse incomplète\",\n \"en_attente\": faisable}","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f7743e0c-70ce-4b6c-b63f-8d30b46bda5b","timestamp":"2026-08-24T21:55:56.581Z","tool_use_result":{"stdout":"# ---------------------------------------------------------------------------\n# Commodités -> tags canoniques (details JSON)\n# ---------------------------------------------------------------------------\n# Chaque règle : clé canonique -> (motifs positifs, motifs négatifs).\n# Les motifs sont matchés sur du texte sans accents en minuscules.\n# Absence de match = inconnu (la clé n'apparaît pas), jamais « false » deviné.\n\n_AMENITY_RULES: dict[str, tuple[str, str | None]] = {\n # inclusions\n \"heating\": (r\"chauffage|chauffe[e]? |chauffe[e]?$|heating|heat included|\\bheat\\b\",\n r\"chauffage non inclus|sans chauffage|non[- ]chauffe|pas chauffe|heat pump\"\n r\"|chauffage[^|.]{0,60}charge d(?:u|es) locataires?\"),\n \"electricity\": (r\"electricite|hydro inclus|electricity|eclaire\\b|\\bhydro\\b\",\n r\"electricite non incluse?|non[- ]eclaire\"\n r\"|electricite[^|.]{0,60}charge d(?:u|es) locataires?\"),\n \"hot_water\": (r\"eau chaude|hot water\",\n r\"eau chaude non incluse?|eau chaude[^|.]{0,60}charge d(?:u|es) locataires?\"),\n \"internet\": (r\"internet|wi[- ]?fi\", r\"internet non inclus\"),\n \"cable\": (r\"\\bcable\\b|television|telus|videotron|bell fibe\", None),\n # électroménagers\n \"fridge\": (r\"refrigerateur|refrigerator|\\bfrigo\\b|\\bfridge\\b|electromenagers?( inclus)?|appliances\", None),\n \"stove\": (r\"cuisiniere|\\bstove\\b|\\bfour\\b|plaques? de cuisson\", None),\n \"dishwasher\": (r\"lave[- ]vaisselle|dishwasher\", r\"entrees? (?:pour |de )?lave[- ]vaisselle|dishwasher hookup\"),\n \"dishwasher_hookup\": (r\"entrees? (?:pour |de )?lave[- ]vaisselle|dishwasher hookups?\", None),\n \"washer_dryer\": (r\"laveuse[- /]secheuse|laveuse et secheuse|\\bwasher\\b|\\bdryer\\b\",\n r\"entrees?[^|.]{0,35}laveuse|washer.{0,10}hookup\"),\n \"washer_dryer_hookup\": (r\"entrees?[^|.]{0,35}laveuse|sorties? laveuse|washer.{0,10}hookups?\", None),\n \"ac\": (r\"climatis|air climatise|\\ba/?c\\b|air conditioning|central air|cooling\\s*:|thermopompe\", None),\n # immeuble\n \"elevator\": (r\"ascen[cs]eur|elevator\", None),\n \"balcony\": (r\"balcon|terrasse|terrace|patio|loggia\",\n r\"pas de balcon|sans balcon|no balcony|aucun balcon\"),\n \"pool\": (r\"piscine|\\bpool\\b|\\bspa\\b\", None),\n \"gym\": (r\"\\bgym\\b|salle d.?entrainement|entrainement|fitness|salle de sport\", None),\n \"laundry\": (r\"buanderie|salle de lavage|laundry (?:room|facilit)\", None),\n \"storage\": (r\"rangement|\\blocker\\b|espace de rangement|storage\", None),\n \"furnished_kw\": (r\"\\bmeuble\\b|\\bmeublee?s?\\b|furnished\", r\"non[- ]meuble|unfurnished\"),\n \"parking_kw\": (r\"stationnement|parking|garage\", None),\n}\n\n_PETS_YES_RE = re.compile(\n r\"animaux (?:de compagnie )?(?:acceptes|admis|bienvenus|permis|autorises|ok)\"\n r\"|pet[s]?[- ]friendly|pets? (?:allowed|welcome)\"\n r\"|chats? (?:et chiens? )?(?:acceptes?|admis)\"\n r\"|chiens? (?:et chats? )?(?:permis|acceptes?|admis)\"\n r\"|anima(?:l|ux)[^|.]{0,15}(?:bienvenus?|acceptes?|admis|permis)\")\n# les deux espèces explicitement acceptées : « oui » sans ambiguïté,\n# testé AVANT les motifs « conditions » (sinon « chats acceptés » l'emporte)\n_PETS_BOTH_RE = re.compile(\n r\"(?:chiens? et chats?|chats? et chiens?) (?:acceptes|admis|bienvenus|permis|autorises)\")\n_PETS_NO_RE = re.compile(\n r\"(?:pas d.?animaux|aucun animal|animaux (?:refuses|interdits|non admis|non acceptes|non autorises|non permis))|no pets?\"\n r\"|animaux (?:permis|acceptes|admis|autorises)?\\s*:\\s*non\"\n r\"|animaux[^|.]{0,15}interdits\")\n_PETS_COND_RE = re.compile(\n r\"animaux (?:sous conditions?|sur approbation)|petits? animaux|petits? chiens?\"\n269:def estimate(civic: str | None = None, postal: str | None = None,\ndef estimate(civic: str | None = None, postal: str | None = None,\n adresse: str | None = None, lat: float | None = None,\n lng: float | None = None, uid: str | None = None,\n solve: bool = True) -> dict:\n \"\"\"Coût d'électricité annuel estimé à une adresse (cache 6 mois).\n\n `solve=False` : ne consulte QUE le cache (aucune résolution de captcha,\n aucun coût) — utilisé pour l'affichage automatique des fiches.\n Le code postal est extrait de l'adresse, ou dérivé du couple lat/lng par\n géocodage inverse Mapbox si l'adresse n'en contient pas (cas d'Immo-Ka).\n Le cache est indexé par `uid` (clé stable de l'annonce) ET par\n civique|code postal, pour que l'affichage cache-only fonctionne même\n quand l'adresse ne porte pas de code postal.\n \"\"\"\n # lecture cache par uid d'abord (ne nécessite ni CP ni Mapbox)\n if uid:\n con = _connect()\n row = con.execute(\n \"SELECT * FROM hydro_cache WHERE uid=? AND cout_annuel IS NOT NULL \"\n \"AND fetched_at>?\", (uid, time.time() - TTL)).fetchone()\n con.close()\n if row:\n return {\"disponible\": True, \"adresse\": row[\"adresse\"],\n \"cout_annuel\": row[\"cout_annuel\"],\n \"cout_mensuel\": row[\"cout_mensuel\"],\n \"kwh_annuel\": row[\"kwh\"], \"cache\": True}\n\n if not civic and adresse:\n civic = _civic(adresse)\n if not postal and adresse:\n cp = re.search(r\"([A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d)\", adresse)\n if cp:\n postal = cp.group(1)\n # cache-only : ne pas déclencher Mapbox ; proposer le bouton si un calcul\n # est faisable (civique connu + CP OU coordonnées disponibles)\n if not solve:\n faisable = bool(civic) and (bool(postal) or\n (lat is not None and lng is not None))\n return {\"disponible\": False,\n \"raison\": \"non calculé\" if faisable else \"adresse incomplète\",\n \"en_attente\": faisable}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"9eb1e057-7699-4b27-947d-cb9ceff1a00f","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKVauYdXmYBnG8q3r8a","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2991,"cache_read_input_tokens":161012,"cache_creation":{"ephemeral_5m_input_tokens":2991,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ed24391b-523d-4d50-a009-9055f37cb37c"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKVauYdXmYBnG8q3r8a","type":"message","role":"assistant","content":[{"type":"text","text":"Interfaces confirmées (`hydro_cache` par uid, clés d'inclusions). J'écris `coutreel.py` (coût réel + prix/pi² avec percentile réel) et `hiver.py`."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2991,"cache_read_input_tokens":161012,"cache_creation":{"ephemeral_5m_input_tokens":2991,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"38f35412-2988-4225-82ba-5027b5986613"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKVauYdXmYBnG8q3r8a","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FiL8Fzs83biPBbfkSdbQij","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/coutreel.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# coutreel.py : « Coût réel mensuel Lou-Ka » — loyer + frais non inclus,\n# ligne par ligne, chaque poste étiqueté :\n# included : explicitement inclus dans le bail (details.inclusions)\n# observed : montant affiché par la source (ex. prix du stationnement)\n# estimated : estimation sourcée (Hydro-Québec via hydro.py, médiane Internet)\n# unknown : ni inclus ni estimable — dit explicitement, jamais inventé\n#\n# + prix au pi² avec percentile RÉELLEMENT calculé sur les comparables\n# (même ville + même nombre de chambres, et rayon ~2 km si géocodée).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\n\nfrom . import db\n\n# Internet résidentiel : médiane des forfaits de base au Québec (ordre de\n# grandeur public CRTC/fournisseurs). Estimation générique, PAS une donnée\n# de l'adresse — toujours présentée comme telle.\nINTERNET_ESTIME = 60.0\n\nVERSION = \"cout-reel-1.0\"\n\n\ndef _pctile(values: list[float], x: float) -> int:\n below = sum(1 for v in values if v < x)\n return round(100 * below / len(values))\n\n\ndef pi2(con, l: dict) -> dict | None:\n \"\"\"Prix au pi² + percentile parmi les comparables (calcul réel).\"\"\"\n price, area = l.get(\"price\"), l.get(\"area_sqft\")\n if not price or not area or area < 100:\n return None\n val = price / area\n out: dict = {\"valeur\": round(val, 2), \"statut\": \"calculated\"}\n\n base = (\"SELECT price, area_sqft, lat, lng FROM listings\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND price IS NOT NULL AND area_sqft > 100 AND uid<>?\")\n args: list = [l[\"uid\"]]\n if l.get(\"city\"):\n base += \" AND city=?\"; args.append(l[\"city\"])\n if l.get(\"bedrooms\") is not None:\n base += \" AND bedrooms=?\"; args.append(l[\"bedrooms\"])\n elif l.get(\"unit_type\"):\n base += \" AND unit_type=?\"; args.append(l[\"unit_type\"])\n rows = con.execute(base, args).fetchall()\n ville = [r[\"price\"] / r[\"area_sqft\"] for r in rows]\n if len(ville) >= 12:\n out[\"percentile_ville\"] = _pctile(ville, val)\n out[\"n_ville\"] = len(ville)\n out[\"portee_ville\"] = (f\"{l.get('city')}, \"\n + (f\"{int(l['bedrooms'])} ch.\"\n if l.get(\"bedrooms\") is not None\n else l.get(\"unit_type\") or \"tous types\"))\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n if lat is not None and lng is not None:\n secteur = [r[\"price\"] / r[\"area_sqft\"] for r in rows\n if r[\"lat\"] is not None\n and abs(r[\"lat\"] - lat) < 0.018 and abs(r[\"lng\"] - lng) < 0.025]\n if len(secteur) >= 12:\n out[\"percentile_secteur\"] = _pctile(secteur, val)\n out[\"n_secteur\"] = len(secteur)\n if \"percentile_ville\" not in out and \"percentile_secteur\" not in out:\n out[\"percentile_note\"] = \"comparables insuffisants pour un percentile\"\n return out\n\n\ndef compute(uid: str, con=None) -> dict | None:\n \"\"\"Coût réel mensuel d'une annonce — module « Coût réel Lou-Ka ».\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n row = con.execute(\n \"SELECT uid, price, area_sqft, city, bedrooms, unit_type, lat, lng,\"\n \" details, address FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n return None\n l = dict(row)\n try:\n details = json.loads(l.get(\"details\") or \"{}\") or {}\n except (ValueError, TypeError):\n details = {}\n inc = details.get(\"inclusions\") or {}\n price = l.get(\"price\")\n\n lignes: list[dict] = []\n lignes.append({\"poste\": \"Loyer affiché\", \"statut\": \"observed\",\n \"montant\": price, \"source\": \"annonce\"})\n\n # --- électricité + chauffage (souvent le même compteur au Québec) ----\n hydro_est = None\n if not inc.get(\"electricity\"):\n from . import hydro\n h = hydro.estimate(uid=uid, adresse=l.get(\"address\"),\n lat=l.get(\"lat\"), lng=l.get(\"lng\"), solve=False)\n if h.get(\"disponible\") and h.get(\"cout_mensuel\"):\n hydro_est = round(float(h[\"cout_mensuel\"]))\n if inc.get(\"electricity\"):\n lignes.append({\"poste\": \"Électricité\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (incluse)\"})\n elif inc.get(\"electricity\") is False:\n lignes.append({\"poste\": \"Électricité\",\n \"statut\": \"estimated\" if hydro_est else \"unknown\",\n \"montant\": hydro_est,\n \"source\": (\"outil public d'estimation Hydro-Québec\"\n if hydro_est else None),\n \"note\": None if hydro_est else\n \"explicitement à la charge du locataire, \"\n \"montant non estimable pour cette adresse\"})\n else:\n lignes.append({\"poste\": \"Électricité\",\n \"statut\": \"estimated\" if hydro_est else \"unknown\",\n \"montant\": hydro_est,\n \"source\": (\"outil public d'estimation Hydro-Québec\"\n if hydro_est else None),\n \"note\": None if hydro_est else\n \"l'annonce ne précise pas si elle est incluse\"})\n\n if inc.get(\"heating\"):\n lignes.append({\"poste\": \"Chauffage\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (inclus)\"})\n elif hydro_est:\n lignes.append({\"poste\": \"Chauffage\", \"statut\": \"estimated\",\n \"montant\": None,\n \"note\": \"chauffage électrique répandu au Québec — \"\n \"déjà compté dans l'estimation d'électricité\"})\n else:\n lignes.append({\"poste\": \"Chauffage\", \"statut\": \"unknown\",\n \"montant\": None,\n \"note\": \"ni inclus ni estimable avec les données \"\n \"disponibles\"})\n\n if inc.get(\"hot_water\"):\n lignes.append({\"poste\": \"Eau chaude\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (incluse)\"})\n else:\n lignes.append({\"poste\": \"Eau chaude\", \"statut\": \"unknown\",\n \"montant\": None,\n \"note\": \"l'annonce ne précise pas si elle est incluse\"})\n\n if inc.get(\"internet\"):\n lignes.append({\"poste\": \"Internet\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (inclus)\"})\n else:\n lignes.append({\"poste\": \"Internet\", \"statut\": \"estimated\",\n \"montant\": INTERNET_ESTIME,\n \"source\": \"médiane des forfaits résidentiels de base \"\n \"au Québec (estimation générique)\"})\n\n parking = details.get(\"parking\") or {}\n if parking.get(\"available\"):\n if parking.get(\"included\"):\n lignes.append({\"poste\": \"Stationnement\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (inclus)\"})\n elif parking.get(\"price\"):\n lignes.append({\"poste\": \"Stationnement\", \"statut\": \"observed\",\n \"montant\": float(parking[\"price\"]),\n \"source\": \"annonce (prix affiché)\"})\n else:\n lignes.append({\"poste\": \"Stationnement\", \"statut\": \"unknown\",\n \"montant\": None,\n \"note\": \"disponible, prix non précisé par la source\"})\n\n # --- totaux : jamais de montant inventé ------------------------------\n total = None\n if price:\n total = float(price) + sum(\n (li[\"montant\"] or 0) for li in lignes[1:]\n if li[\"statut\"] in (\"observed\", \"estimated\")\n and li[\"montant\"] is not None)\n inconnues = [li[\"poste\"] for li in lignes if li[\"statut\"] == \"unknown\"]\n\n out = {\n \"uid\": uid, \"version\": VERSION,\n \"loyer\": price,\n \"lignes\": lignes,\n \"total_estime\": round(total) if total else None,\n \"annuel_estime\": round(total * 12) if total else None,\n \"postes_inconnus\": inconnues,\n \"pi2\": pi2(con, l),\n \"methode\": (\"loyer affiché + frais non inclus connus (observés) ou \"\n \"estimés avec source ; les postes inconnus sont listés \"\n \"tels quels, jamais chiffrés arbitrairement\"),\n }\n if total and l.get(\"area_sqft\") and l[\"area_sqft\"] > 100:\n out[\"total_pi2\"] = round(total / l[\"area_sqft\"], 2)\n return out\n finally:\n if own:\n con.close()\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2991,"cache_read_input_tokens":161012,"cache_creation":{"ephemeral_5m_input_tokens":2991,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"44c96bf4-dc11-4a6a-b515-696aee49e564"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FiL8Fzs83biPBbfkSdbQij","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/coutreel.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"574346d2-9512-4112-8a1f-6c4b66ce1bca","timestamp":"2026-08-24T21:56:51.213Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/coutreel.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# coutreel.py : « Coût réel mensuel Lou-Ka » — loyer + frais non inclus,\n# ligne par ligne, chaque poste étiqueté :\n# included : explicitement inclus dans le bail (details.inclusions)\n# observed : montant affiché par la source (ex. prix du stationnement)\n# estimated : estimation sourcée (Hydro-Québec via hydro.py, médiane Internet)\n# unknown : ni inclus ni estimable — dit explicitement, jamais inventé\n#\n# + prix au pi² avec percentile RÉELLEMENT calculé sur les comparables\n# (même ville + même nombre de chambres, et rayon ~2 km si géocodée).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\n\nfrom . import db\n\n# Internet résidentiel : médiane des forfaits de base au Québec (ordre de\n# grandeur public CRTC/fournisseurs). Estimation générique, PAS une donnée\n# de l'adresse — toujours présentée comme telle.\nINTERNET_ESTIME = 60.0\n\nVERSION = \"cout-reel-1.0\"\n\n\ndef _pctile(values: list[float], x: float) -> int:\n below = sum(1 for v in values if v < x)\n return round(100 * below / len(values))\n\n\ndef pi2(con, l: dict) -> dict | None:\n \"\"\"Prix au pi² + percentile parmi les comparables (calcul réel).\"\"\"\n price, area = l.get(\"price\"), l.get(\"area_sqft\")\n if not price or not area or area < 100:\n return None\n val = price / area\n out: dict = {\"valeur\": round(val, 2), \"statut\": \"calculated\"}\n\n base = (\"SELECT price, area_sqft, lat, lng FROM listings\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND price IS NOT NULL AND area_sqft > 100 AND uid<>?\")\n args: list = [l[\"uid\"]]\n if l.get(\"city\"):\n base += \" AND city=?\"; args.append(l[\"city\"])\n if l.get(\"bedrooms\") is not None:\n base += \" AND bedrooms=?\"; args.append(l[\"bedrooms\"])\n elif l.get(\"unit_type\"):\n base += \" AND unit_type=?\"; args.append(l[\"unit_type\"])\n rows = con.execute(base, args).fetchall()\n ville = [r[\"price\"] / r[\"area_sqft\"] for r in rows]\n if len(ville) >= 12:\n out[\"percentile_ville\"] = _pctile(ville, val)\n out[\"n_ville\"] = len(ville)\n out[\"portee_ville\"] = (f\"{l.get('city')}, \"\n + (f\"{int(l['bedrooms'])} ch.\"\n if l.get(\"bedrooms\") is not None\n else l.get(\"unit_type\") or \"tous types\"))\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n if lat is not None and lng is not None:\n secteur = [r[\"price\"] / r[\"area_sqft\"] for r in rows\n if r[\"lat\"] is not None\n and abs(r[\"lat\"] - lat) < 0.018 and abs(r[\"lng\"] - lng) < 0.025]\n if len(secteur) >= 12:\n out[\"percentile_secteur\"] = _pctile(secteur, val)\n out[\"n_secteur\"] = len(secteur)\n if \"percentile_ville\" not in out and \"percentile_secteur\" not in out:\n out[\"percentile_note\"] = \"comparables insuffisants pour un percentile\"\n return out\n\n\ndef compute(uid: str, con=None) -> dict | None:\n \"\"\"Coût réel mensuel d'une annonce — module « Coût réel Lou-Ka ».\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n row = con.execute(\n \"SELECT uid, price, area_sqft, city, bedrooms, unit_type, lat, lng,\"\n \" details, address FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n return None\n l = dict(row)\n try:\n details = json.loads(l.get(\"details\") or \"{}\") or {}\n except (ValueError, TypeError):\n details = {}\n inc = details.get(\"inclusions\") or {}\n price = l.get(\"price\")\n\n lignes: list[dict] = []\n lignes.append({\"poste\": \"Loyer affiché\", \"statut\": \"observed\",\n \"montant\": price, \"source\": \"annonce\"})\n\n # --- électricité + chauffage (souvent le même compteur au Québec) ----\n hydro_est = None\n if not inc.get(\"electricity\"):\n from . import hydro\n h = hydro.estimate(uid=uid, adresse=l.get(\"address\"),\n lat=l.get(\"lat\"), lng=l.get(\"lng\"), solve=False)\n if h.get(\"disponible\") and h.get(\"cout_mensuel\"):\n hydro_est = round(float(h[\"cout_mensuel\"]))\n if inc.get(\"electricity\"):\n lignes.append({\"poste\": \"Électricité\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (incluse)\"})\n elif inc.get(\"electricity\") is False:\n lignes.append({\"poste\": \"Électricité\",\n \"statut\": \"estimated\" if hydro_est else \"unknown\",\n \"montant\": hydro_est,\n \"source\": (\"outil public d'estimation Hydro-Québec\"\n if hydro_est else None),\n \"note\": None if hydro_est else\n \"explicitement à la charge du locataire, \"\n \"montant non estimable pour cette adresse\"})\n else:\n lignes.append({\"poste\": \"Électricité\",\n \"statut\": \"estimated\" if hydro_est else \"unknown\",\n \"montant\": hydro_est,\n \"source\": (\"outil public d'estimation Hydro-Québec\"\n if hydro_est else None),\n \"note\": None if hydro_est else\n \"l'annonce ne précise pas si elle est incluse\"})\n\n if inc.get(\"heating\"):\n lignes.append({\"poste\": \"Chauffage\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (inclus)\"})\n elif hydro_est:\n lignes.append({\"poste\": \"Chauffage\", \"statut\": \"estimated\",\n \"montant\": None,\n \"note\": \"chauffage électrique répandu au Québec — \"\n \"déjà compté dans l'estimation d'électricité\"})\n else:\n lignes.append({\"poste\": \"Chauffage\", \"statut\": \"unknown\",\n \"montant\": None,\n \"note\": \"ni inclus ni estimable avec les données \"\n \"disponibles\"})\n\n if inc.get(\"hot_water\"):\n lignes.append({\"poste\": \"Eau chaude\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (incluse)\"})\n else:\n lignes.append({\"poste\": \"Eau chaude\", \"statut\": \"unknown\",\n \"montant\": None,\n \"note\": \"l'annonce ne précise pas si elle est incluse\"})\n\n if inc.get(\"internet\"):\n lignes.append({\"poste\": \"Internet\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (inclus)\"})\n else:\n lignes.append({\"poste\": \"Internet\", \"statut\": \"estimated\",\n \"montant\": INTERNET_ESTIME,\n \"source\": \"médiane des forfaits résidentiels de base \"\n \"au Québec (estimation générique)\"})\n\n parking = details.get(\"parking\") or {}\n if parking.get(\"available\"):\n if parking.get(\"included\"):\n lignes.append({\"poste\": \"Stationnement\", \"statut\": \"included\",\n \"montant\": 0, \"source\": \"annonce (inclus)\"})\n elif parking.get(\"price\"):\n lignes.append({\"poste\": \"Stationnement\", \"statut\": \"observed\",\n \"montant\": float(parking[\"price\"]),\n \"source\": \"annonce (prix affiché)\"})\n else:\n lignes.append({\"poste\": \"Stationnement\", \"statut\": \"unknown\",\n \"montant\": None,\n \"note\": \"disponible, prix non précisé par la source\"})\n\n # --- totaux : jamais de montant inventé ------------------------------\n total = None\n if price:\n total = float(price) + sum(\n (li[\"montant\"] or 0) for li in lignes[1:]\n if li[\"statut\"] in (\"observed\", \"estimated\")\n and li[\"montant\"] is not None)\n inconnues = [li[\"poste\"] for li in lignes if li[\"statut\"] == \"unknown\"]\n\n out = {\n \"uid\": uid, \"version\": VERSION,\n \"loyer\": price,\n \"lignes\": lignes,\n \"total_estime\": round(total) if total else None,\n \"annuel_estime\": round(total * 12) if total else None,\n \"postes_inconnus\": inconnues,\n \"pi2\": pi2(con, l),\n \"methode\": (\"loyer affiché + frais non inclus connus (observés) ou \"\n \"estimés avec source ; les postes inconnus sont listés \"\n \"tels quels, jamais chiffrés arbitrairement\"),\n }\n if total and l.get(\"area_sqft\") and l[\"area_sqft\"] > 100:\n out[\"total_pi2\"] = round(total / l[\"area_sqft\"], 2)\n return out\n finally:\n if own:\n con.close()\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKVauYdXmYBnG8q3r8a","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SkFJ7EtV6n8jzrQBbA4rMK","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/hiver.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# hiver.py : « Vie quotidienne en hiver » — indicateur 0-100 pensé pour le\n# Québec : peut-on vivre son quotidien À PIED quand il fait -20 °C\n# et que les trottoirs sont enneigés?\n#\n# Calcul déterministe et explicable à partir des données DÉJÀ collectées\n# (poi_cache — plus proche commodité par catégorie, OpenStreetMap) :\n# épicerie, pharmacie, arrêt de bus/métro, dépanneur. Chaque sous-score est\n# une décroissance linéaire de la distance de marche (vol d'oiseau × 1,3).\n# Pas de données de pente/exposition disponibles -> non pris en compte, et la\n# méthodologie l'assume. Sans données POI : aucun score (jamais inventé).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\n\nfrom . import db\n\nVERSION = \"hiver-1.0\"\nDETOUR = 1.3 # vol d'oiseau -> distance de marche (même facteur que POI)\nVITESSE_M_MIN = 80.0 # 4,8 km/h\n\n# (catégorie, libellé, poids, distance pleine note (m), distance note nulle (m))\nCRITERES = [\n (\"epicerie\", \"Épicerie\", 0.35, 300, 1500),\n (\"pharmacie\", \"Pharmacie\", 0.20, 300, 1500),\n (\"bus\", \"Arrêt de bus\", 0.25, 150, 900),\n (\"metro\", \"Métro\", 0.05, 400, 1800),\n (\"depanneur\", \"Dépanneur\", 0.15, 200, 1000),\n]\n\nCLASSES = [(80, \"très pratique\"), (60, \"pratique\"), (40, \"exigeant\")]\nCLASSE_MIN = \"difficile\"\n\n\ndef _sous_score(dist_m: float, plein: float, nul: float) -> float:\n d = dist_m * DETOUR\n if d <= plein:\n return 100.0\n if d >= nul:\n return 0.0\n return 100.0 * (nul - d) / (nul - plein)\n\n\ndef score(lat: float | None, lng: float | None, con=None) -> dict | None:\n \"\"\"Score hiver de l'immeuble (via poi_cache) — None si données absentes.\"\"\"\n if lat is None or lng is None:\n return None\n own = con is None\n if own:\n con = db.connect()\n try:\n key = f\"{round(lat, 4)},{round(lng, 4)}\"\n row = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\",\n (key,)).fetchone()\n if row is None:\n return None\n try:\n pois = {p[\"cat\"]: p for p in json.loads(row[\"pois\"] or \"[]\")}\n except (ValueError, TypeError):\n return None\n if not pois:\n return None\n\n total = 0.0\n poids_total = 0.0\n detail = []\n for cat, label, poids, plein, nul in CRITERES:\n p = pois.get(cat)\n if p is None:\n # métro absent = normal hors Montréal : critère simplement omis\n if cat == \"metro\":\n continue\n s = 0.0\n item = {\"critere\": label, \"score\": 0, \"distance_m\": None,\n \"note\": \"aucun repéré à distance de marche\"}\n else:\n s = _sous_score(p[\"dist_m\"], plein, nul)\n item = {\"critere\": label, \"score\": round(s),\n \"distance_m\": p[\"dist_m\"],\n \"minutes\": max(1, round(p[\"dist_m\"] * DETOUR\n / VITESSE_M_MIN)),\n \"nom\": p.get(\"name\")}\n total += s * poids\n poids_total += poids\n detail.append(item)\n if poids_total == 0:\n return None\n note = round(total / poids_total)\n classe = CLASSE_MIN\n for seuil, nom in CLASSES:\n if note >= seuil:\n classe = nom\n break\n return {\n \"score\": note, \"classe\": classe, \"detail\": detail,\n \"statut\": \"calculated\", \"version\": VERSION,\n \"methode\": (\"commodités essentielles à distance de marche \"\n \"(épicerie, pharmacie, bus/métro, dépanneur — \"\n \"OpenStreetMap), décroissance linéaire de la distance \"\n \"de marche estimée (vol d'oiseau × 1,3). Pente et \"\n \"exposition non disponibles : non prises en compte.\"),\n }\n finally:\n if own:\n con.close()\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2991,"cache_read_input_tokens":161012,"cache_creation":{"ephemeral_5m_input_tokens":2991,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f4b70ddd-5d09-473a-a1c8-e65837bb098e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SkFJ7EtV6n8jzrQBbA4rMK","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/hiver.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a8bfc8de-7573-4a2a-a642-c2f25d4015b0","timestamp":"2026-08-24T21:57:08.683Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/hiver.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# hiver.py : « Vie quotidienne en hiver » — indicateur 0-100 pensé pour le\n# Québec : peut-on vivre son quotidien À PIED quand il fait -20 °C\n# et que les trottoirs sont enneigés?\n#\n# Calcul déterministe et explicable à partir des données DÉJÀ collectées\n# (poi_cache — plus proche commodité par catégorie, OpenStreetMap) :\n# épicerie, pharmacie, arrêt de bus/métro, dépanneur. Chaque sous-score est\n# une décroissance linéaire de la distance de marche (vol d'oiseau × 1,3).\n# Pas de données de pente/exposition disponibles -> non pris en compte, et la\n# méthodologie l'assume. Sans données POI : aucun score (jamais inventé).\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\n\nfrom . import db\n\nVERSION = \"hiver-1.0\"\nDETOUR = 1.3 # vol d'oiseau -> distance de marche (même facteur que POI)\nVITESSE_M_MIN = 80.0 # 4,8 km/h\n\n# (catégorie, libellé, poids, distance pleine note (m), distance note nulle (m))\nCRITERES = [\n (\"epicerie\", \"Épicerie\", 0.35, 300, 1500),\n (\"pharmacie\", \"Pharmacie\", 0.20, 300, 1500),\n (\"bus\", \"Arrêt de bus\", 0.25, 150, 900),\n (\"metro\", \"Métro\", 0.05, 400, 1800),\n (\"depanneur\", \"Dépanneur\", 0.15, 200, 1000),\n]\n\nCLASSES = [(80, \"très pratique\"), (60, \"pratique\"), (40, \"exigeant\")]\nCLASSE_MIN = \"difficile\"\n\n\ndef _sous_score(dist_m: float, plein: float, nul: float) -> float:\n d = dist_m * DETOUR\n if d <= plein:\n return 100.0\n if d >= nul:\n return 0.0\n return 100.0 * (nul - d) / (nul - plein)\n\n\ndef score(lat: float | None, lng: float | None, con=None) -> dict | None:\n \"\"\"Score hiver de l'immeuble (via poi_cache) — None si données absentes.\"\"\"\n if lat is None or lng is None:\n return None\n own = con is None\n if own:\n con = db.connect()\n try:\n key = f\"{round(lat, 4)},{round(lng, 4)}\"\n row = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\",\n (key,)).fetchone()\n if row is None:\n return None\n try:\n pois = {p[\"cat\"]: p for p in json.loads(row[\"pois\"] or \"[]\")}\n except (ValueError, TypeError):\n return None\n if not pois:\n return None\n\n total = 0.0\n poids_total = 0.0\n detail = []\n for cat, label, poids, plein, nul in CRITERES:\n p = pois.get(cat)\n if p is None:\n # métro absent = normal hors Montréal : critère simplement omis\n if cat == \"metro\":\n continue\n s = 0.0\n item = {\"critere\": label, \"score\": 0, \"distance_m\": None,\n \"note\": \"aucun repéré à distance de marche\"}\n else:\n s = _sous_score(p[\"dist_m\"], plein, nul)\n item = {\"critere\": label, \"score\": round(s),\n \"distance_m\": p[\"dist_m\"],\n \"minutes\": max(1, round(p[\"dist_m\"] * DETOUR\n / VITESSE_M_MIN)),\n \"nom\": p.get(\"name\")}\n total += s * poids\n poids_total += poids\n detail.append(item)\n if poids_total == 0:\n return None\n note = round(total / poids_total)\n classe = CLASSE_MIN\n for seuil, nom in CLASSES:\n if note >= seuil:\n classe = nom\n break\n return {\n \"score\": note, \"classe\": classe, \"detail\": detail,\n \"statut\": \"calculated\", \"version\": VERSION,\n \"methode\": (\"commodités essentielles à distance de marche \"\n \"(épicerie, pharmacie, bus/métro, dépanneur — \"\n \"OpenStreetMap), décroissance linéaire de la distance \"\n \"de marche estimée (vol d'oiseau × 1,3). Pente et \"\n \"exposition non disponibles : non prises en compte.\"),\n }\n finally:\n if own:\n con.close()\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"2a0cf0d0-acba-4d3d-a4fa-55d7d3ef8449","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"compacting","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"32edd852-ec87-4e01-8e6f-44f528d8e42a"}} {"type":"claude","event":{"type":"system","subtype":"status","status":null,"compact_result":"success","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c1c98925-c868-471c-a88e-2690acf1bea0"}} {"type":"claude","event":{"type":"system","subtype":"compact_boundary","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"12666bb8-605c-4746-aac5-9698341b29a6","compact_metadata":{"trigger":"auto","pre_tokens":174525,"post_tokens":14020,"duration_ms":124714}}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\nSummary:\n1. Primary Request and Intent:\nTransform the existing Lou-Ka project (`~/apps/lou-ka`, lou-ka.com — Quebec rental listing aggregator, in production) from enriched listing pages into \"dossiers d'intelligence locative\" / a \"passeport numérique du logement et de l'immeuble\". The user's 29-section mission mandates:\n- **Audit first** (stack, data models, crawlers, normalization, enrichments, jobs, caches, unexploited fields), create a technical plan, **then actually implement** — reuse existing architecture, never rebuild what exists.\n- **Fundamental principle**: data > decoration. Every datum classified `observed`/`calculated`/`estimated`/`inferred`/`user_reported`, with source, dates, confidence, method, algorithm version. Never present estimates as facts; never invent amounts; state unknowns explicitly.\n- Features: §3 Coût réel mensuel; §4 Prix/pi² with really-computed percentile; §5 Historique des annonces (snapshots/changes/timeline); §6 Détection annonces recyclées (multi-signal similarity score, no auto-merge below confidence); §7 Passeport de l'immeuble (persistent Building entity via address normalization); §8 Score de rotation (only what Lou-Ka observes, methodology displayed); §9 Pression sur les loyers (no indicator with insufficient sample); §10 Loyer précédent probable (conservative); §11-§21 photo analysis, sunlight, vis-à-vis, noise, winter score, isochrones, personal destinations, climate comfort, greenery, construction, internet (implement only what's defensible with available data); §22-§29 Gestionnaire/propriétaire entity + SerpApi Google Maps resolution (with `manager_match_confidence`, never blindly pick first result) + Google Maps Reviews sync (pagination via `next_page_token`, `hl=fr`, newest-first, local cache/DB, never call API per page view), review data model with dedup, sync service with retry/timeout/backoff, analytics (distribution, recency, trends, NOT just Google average), review intelligence (topics/sentiment/severity/confidence).\n- **SerpApi key** `cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33` provided; must be used ONLY via `SERPAPI_API_KEY` env var: never hardcoded, never committed, never exposed to frontend/browser/logs; add `.env.example` with `SERPAPI_API_KEY=`; integration exclusively server-side.\n\n2. Key Technical Concepts:\n- Backend: Python 3.14 + FastAPI + uvicorn, port 8095 prod, PM2 (`lou-ka-web`: `run.py serve 8095`; `lou-ka-sync`: `run.py watch 60`; `lou-ka-ngrok`), SQLite WAL (`data/louka.db` ~1.5GB), raw SQL, auto-migrations in `db.py connect()` (_SCHEMA executescript + _MIGRATIONS ALTER TABLE)\n- Frontend: React 18.3 + TypeScript + Vite (`frontend/`), React Router v6, Mapbox GL + `@groupe-ka/ka-maps`, fiche = `pages/Listing.tsx` with strict DOM-order convention (2 `f-col` columns, `f-bloc` sections), fetch-on-mount block components (pattern: `HistoriqueTAL.tsx`)\n- Existing infra reused: `dedup._parse_address()` (address key \"civique|rue|ville\" + unit number), `PORTALS`/`CLASSIFIEDS` authority sets, `price_log` (price history since v1), `image_checks.sha1` (photo content hashes), `hydro.db`/`hydro_cache` (cout_mensuel by uid, `hydro.estimate(uid=…, solve=False)` cache-only), `poi_cache` by coord_key (nearest POI per category), `details.inclusions` keys (heating/electricity/hot_water/internet), `details.parking` (available/included/price), coord_key = \"lat,lng\" rounded 4 decimals, `ingest.watch()` loop ordering (geocode→poi→quartier→quality→imgcheck→dedup→fairvalue→kascores→hydro→tal)\n- env loading: `run.py` parses `.env` manually (no dotenv), `os.environ.setdefault`\n- ~61k active listings, 250 connectors, sources.json = `{\"sources\": [{id, name, url, listing_url, sectors, connector, status, region}]}`\n- Decisions: NOT implementing ensoleillement/vis-à-vis (no building heights), network isochrones (no routing engine), AI photo analysis (no multimodal model wired), permits/chantiers, internet infra — to be documented as excluded (no pseudo-science)\n\n3. Files and Code Sections:\n- **`.env`** (gitignored ✓): appended `SERPAPI_API_KEY=cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33` with comment\n- **`.env.example`** (created): all env vars empty incl. `SERPAPI_API_KEY=` with \"Server-side SEULEMENT\" note\n- **`louka/db.py`** (edited — core migrations + event logging):\n - _SCHEMA additions: `listing_events(id, uid, ts, event, old, new)` + `idx_levents_uid(uid,ts)`; `buildings(bkey PK, address, city, lat, lng, stats JSON, version, computed_at)`; `recycled_cache(uid PK, matches, computed_at)`; `managers(id, source_id UNIQUE, name, aliases, website, phone, gmaps_place_id, gmaps_data_id, gmaps_name, gmaps_address, gmaps_rating, gmaps_reviews, match_confidence, match_signals, resolved_at, resolve_failed, review_stats, last_synced_at)`; `manager_reviews(id, manager_id, external_review_id, source DEFAULT 'google_maps', rating, text, published_at, relative_date_raw, author_name, author_review_count, owner_response, owner_response_date, source_url, fetched_at, content_hash, analysis)` + `idx_mreviews_mgr` + UNIQUE `idx_mreviews_ext(manager_id, external_review_id)`\n - _MIGRATIONS listings: `\"building_key\": \"TEXT\"`; connect(): `CREATE INDEX IF NOT EXISTS idx_listings_bkey ON listings(building_key)`\n - New `_j(v)` + `_log_events(con, uid, ts, old, new)`: diffs description (char counts), area_sqft, availability_date, inclusions (key-level diff), images (set compare → {\"n\": count}) → INSERT listing_events\n - sync_source: old-row SELECT extended to `content_hash, price, images, description, area_sqft, availability_date, details, active`; changed branch calls `_log_events(con, lst.uid, now, row, params)` + logs \"reapparition\" if `not row[\"active\"]`; unchanged branch logs \"reapparition\" if inactive; miss-loop logs \"disparition\" when setting active=0\n- **`louka/history.py`** (created): `timeline(uid, con=None)` — merges price_log (type \"prix\", prix_avant chaining) + listing_events (avant/apres JSON-loaded), sorted desc, items[:60], all `statut: \"observed\"`; returns premiere/derniere_observation, active, jours_en_ligne, prix_initial (first non-null price_log), prix_actuel, variation (rounded 4), modifications count, methode string\n- **`louka/building.py`** (created): `VERSION=\"immeuble-1.0\"`; `building_key(address, city, coord_key)` → (`f\"adr:{akey}\"` via `dedup._parse_address` or `f\"geo:{coord_key}\"`, unit); `_stats_for(members, now)` → annonces_total/actives/30j/90j/12m, unites_identifiees (distinct unit numbers), unites_estimees = max(units, actives, 1), loyer_median, loyer_median_par_cc (n≥2 per cc), pi2_median (area>100), pression_loyers (median first_price 12m vs 12-24m, both n≥MIN_ADS_PRESSURE=3, statut \"calculated\"), rotation (ratio = annonces_12m/unites_estimees; classes: <0.5 faible, <1.0 normale, <2.0 élevée, else \"très élevée\"; requires obs_days≥180 & members≥3 else \"donnees_insuffisantes\"; includes methode), gestionnaires (sources ∉ PORTALS∪CLASSIFIEDS), sources counts, unites_actives[:24]; `rollup(con=None)` — full recompute: reads ALL listings (incl. inactive), first prices via price_log subquery, updates listings.building_key where changed, stats on `uniq = [m for m in members if not m[\"dup_of\"]]`, upserts buildings (most-frequent address/city); `fiche(bkey, con=None)` returns row + parsed stats\n- **`louka/recycled.py`** (created): `TTL=7*86400`, `SEUIL_AFFICHAGE=70`, `MAX_CANDIDATES=200`; `lookup(uid, con=None)` — cache-first (recycled_cache), candidates = inactive listings same building_key excluding dup group, must have `last_seen <= cur.first_seen + 7d`; `_score()`: same unit +35 (differ→0,[]), same unit_type +10 (differ→0), same bedrooms +8 (differ→0), area ±3% +15 (else −10), sha1 photo overlap ≥0.5 +30 / >0 +12 (via `image_checks WHERE url IN(...)`), Jaccard tokens ≥0.7 +20 / ≥0.45 +10, same source +5, cap 100; output matches[:5] with {uid, source, prix, unit_type, derniere/premiere_observation, confiance, signaux}, `statut: \"inferred\"`, methode\n- **`louka/coutreel.py`** (created): `INTERNET_ESTIME=60.0`, `VERSION=\"cout-reel-1.0\"`; `pi2(con, l)` — val=price/area, comparables same city + bedrooms (or unit_type), active/published/dup_of NULL/area>100; percentile_ville if n≥12; percentile_secteur via bbox (|Δlat|<0.018, |Δlng|<0.025) if n≥12; `_pctile` = % below; `compute(uid, con=None)` — lignes: Loyer (observed); Électricité (included / estimated via `hydro.estimate(uid=…, solve=False)` cout_mensuel / unknown with explicit notes distinguishing \"à la charge du locataire\" vs \"ne précise pas\"); Chauffage (included / estimated montant None \"déjà compté dans l'estimation d'électricité\" / unknown); Eau chaude (included/unknown); Internet (included / estimated 60$ \"médiane des forfaits… estimation générique\"); Stationnement (included / observed parking.price / unknown \"prix non précisé\"); total_estime = price + Σ(observed|estimated montants), annuel_estime, postes_inconnus, total_pi2, pi2, methode\n- **`louka/hiver.py`** (created): `VERSION=\"hiver-1.0\"`, DETOUR=1.3, VITESSE_M_MIN=80; CRITERES=[(epicerie,0.35,300,1500),(pharmacie,0.20,300,1500),(bus,0.25,150,900),(metro,0.05,400,1800),(depanneur,0.15,200,1000)]; linear decay `_sous_score`; metro omitted (not zeroed) if absent, other missing categories = 0 with note; classes ≥80 \"très pratique\", ≥60 \"pratique\", ≥40 \"exigeant\", else \"difficile\"; `score(lat, lng, con=None)` reads poi_cache by coord_key, returns None without data; statut \"calculated\", methode notes pente/exposition unavailable\n- **Read (context)**: `louka/web.py` (endpoints incl. `/api/listings/{uid}` structure with poi/kascores/quartier/digest/price_history embedding, `_row_to_dict`), `frontend/src/pages/Listing.tsx` (col A: galerie→hero→description→inclusions→pratique; col B: PriceAnalysis→RegistreLoyers→carte→RisqueInondation→HistoriqueTAL→QualiteAir→CommercesProches→EssenceProche→HydroEstimation→KaScoresBlock→quartier→poi), `frontend/src/components/HistoriqueTAL.tsx` (block pattern: null until fetch, `f-bloc` section, zi-badge classes, `fine` methodology footer), `run.py`, `louka/ingest.py`, `louka/dedup.py`, `louka/hydro.py`, `louka/normalize.py` `_AMENITY_RULES`\n\n4. Errors and fixes:\n- **Redundant pi2 computation in `building.py`** (self-caught): `_stats_for` first computed pi2 via `_median` (which rounds to int) then recomputed with `statistics.median`; fixed with an Edit removing the first computation, keeping only `vals_pi2 = [m[\"price\"]/m[\"area\"] …]; pi2 = round(statistics.median(vals_pi2), 2) if vals_pi2 else None`.\n- No user-reported errors or corrective feedback so far.\n\n5. Problem Solving:\n- Full architecture audit completed (Explore agent + direct file reads) establishing precise reuse points.\n- Designed honest, explicable indicators respecting the user's \"no invented data\" rule: rotation with min-sample gates (180d observation, ≥3 ads), pression loyers requiring n≥3 per window, percentiles requiring n≥12 comparables, recycled matching hard-vetoing on contradictory unit/type/bedrooms, coût réel listing unknowns explicitly instead of guessing.\n- Decided exclusions (documented in plan message): ensoleillement, vis-à-vis, network isochrones, AI photo analysis, chantiers/permits, internet infra — data unavailable.\n- Event logging integrated into existing `sync_source` without disturbing price_log (price stays in price_log; timeline merges both).\n\n6. All user messages:\n- Message 1 (the entire mission): \"MISSION — TRANSFORMER LES FICHES LOU-KA EN DOSSIERS D'INTELLIGENCE LOCATIVE\" — full 29-section spec (audit, principle données>décoration with observed/calculated/estimated/inferred/user_reported labeling, coût réel, prix/pi², historique, annonces recyclées, passeport immeuble, rotation, pression loyers, loyer précédent, photos, ensoleillement, vis-à-vis, bruit, score hiver, isochrones, destinations personnelles, confort climatique, verdure, chantiers, internet, gestionnaire section majeure, SerpApi (key via SERPAPI_API_KEY only, .env.example, server-side only), résolution Google Maps avec match confidence, reviews sync paginé, modèle de données reviews, syncManagerReviews service, analyse des avis, review intelligence) — ending with \"avec cle pour sperapi : cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33\"\n- (No other user messages; remainder were tool results and a system summarization request.)\n\n7. Pending Tasks (todo list):\n- [in_progress → effectively done] history module (item 3) — mark completed along with building/recycled/coutreel/hiver (items 4-7, all written)\n- Write `louka/serpapi_client.py` + `louka/managers.py` (item 8)\n- Wire API endpoints in `web.py`, CLI commands in `run.py`, hooks in `ingest.watch()` (item 9)\n- Frontend: `CoutReel.tsx`, `HistoriqueLouka.tsx`, `ImmeubleBloc.tsx`, `GestionnaireBloc.tsx` + `Listing.tsx` integration + `api.ts` fetchers + CSS (item 10)\n- Initial `building.rollup()` + curl tests of new endpoints (item 11)\n- Frontend build, pm2 restart, healthcheck (item 12)\n- Commit + push origin main (item 13)\n\n8. Current Work:\nJust finished creating `louka/coutreel.py` and `louka/hiver.py` (both files written successfully). Todo items 3-7 are functionally complete (db.py migrations + event logging, history.py, building.py, recycled.py, coutreel.py, hiver.py) though the todo list still shows item 3 as in_progress and 4-7 pending — needs a TodoWrite refresh. The immediate next module per the plan: `louka/serpapi_client.py` (server-side SerpApi HTTP client reading `SERPAPI_API_KEY` from env, ~30s timeout, 3 retries with backoff, never logging the key) and `louka/managers.py` with: seed from `data/sources.json` (sources ∉ PORTALS/CLASSIFIEDS from dedup.py), `resolve()` via `engine=google_maps` (q=name+city, google_domain=google.ca, hl=fr, gl=ca) scoring name similarity + website domain + phone + city into `match_confidence` (associate only above threshold ~0.55-0.6, store match_signals, else resolve_failed), `sync_reviews()` via `engine=google_maps_reviews` with data_id/place_id + `next_page_token` pagination + sort newest + `hl=fr` + safety page cap, upsert dedup by (manager_id, external_review_id) + content_hash fallback, `_analyze()` review stats (avg, count, distribution 1-5, recent 12m avg, trend, %1-2 vs %4-5) + French lexicon topic classification (entretien, réparations, communication, personnel, propreté, chauffage, eau chaude, insectes, bruit, sécurité, ascenseurs, stationnement, dépôt/remboursement, augmentation, service client) with sentiment from rating (1-2 neg / 3 neutre / 4-5 pos), severity, confidence stored in analysis JSON + aggregated in managers.review_stats, and `precompute(limit, budget)` for the watch loop.\n\n9. Optional Next Step:\nUpdate the todo list (mark items 3-7 completed, item 8 in_progress), then write `louka/serpapi_client.py` and `louka/managers.py` exactly per the plan quoted in my plan message: \"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google Maps via SerpApi, server-side, analyse de thèmes lexicale\". This directly implements mission sections §22-§29 (gestionnaire entity, SerpApi resolution with `manager_match_confidence`, paginated review sync with `next_page_token`/`hl=fr`, review dedup model, `syncManagerReviews`-style service with retry/timeout/backoff, analytics beyond the Google average, review intelligence with topics/sentiment/severity/confidence). After that: item 9 (web.py endpoints `/api/listings/{uid}/historique`, `/api/listings/{uid}/cout-reel`, `/api/managers/{source_id}`, embed `immeuble` + `hiver` in the listing payload; run.py CLI `buildings`/`managers-sync`; ingest.watch hooks `building.rollup()` after dedup + budgeted `managers.precompute()`), then frontend (item 10) following the `HistoriqueTAL.tsx` f-bloc pattern in `Listing.tsx`.\n\nIf you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/simon-pierreboucher/.claude/projects/-Users-simon-pierreboucher-apps-lou-ka/62f13d0b-8c21-4252-973c-9bb84aed0233.jsonl\nContinue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with \"I'll continue\" or similar. Pick up the last task as if the break never happened."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0e517b3b-9588-4d39-8219-bf4da10a186c","timestamp":"2026-08-24T21:59:13.409Z","isSynthetic":true}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKk61QMnJ2rM7whpN19","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISkwQKpwEIERgCKkCAWlyD2rTZc2Gp5M2spe+ZG05XObVY+R9p4zhYtuum+kDmDmUK6/h+tQ7dS+kFfQdMA+VpxxpmN4HZNbwTLJZ1Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG2/bLUBrABAhIMdY5vvbUjZLDqeVRnGgy8sfV0cKiDHF52hx0iMKk7G6U5q7u0JVHd0r8/jC998HwXuLvoRk0cTBusvWVrFOD4B1KRWlYnIj9pEZYg+iqYAnKNN0UlXnM0DdyWTeWNyA3N8ZTxODg/q+yxmtkZzCwPqZmB4ma02/QumOml7Dh6j1frQIdVkrJksR85kl1ne93VtlX4rHMygu/muSDhKvsqSX0IxVljxZgsYjmv37L7SHWdwsdIFPLxpHs6qqmkcAspMckH4bZIJcC/QcR1iBGdQLkR8MwWvOiCFn4iOBociiOKkNF19Q+4KEAK1+ZbSRmfeFBwhDpG5mLi0L4fCz9efUSCVkXfoRMYUBZyFDTPR2muZzrJUE4fPFQam+tbck6rtYh+qnGEQRXBgq3Sq3m8YBniQV8lY6GgMJJNUL7SctsVLLKhSXF2+xydL9A1xG4X5BPEKzK0cL6PWMuIPJMoNnA8T5X8ao4YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":33694,"cache_read_input_tokens":21718,"cache_creation":{"ephemeral_5m_input_tokens":33694,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4a19f0b2-76ba-47bc-8113-9982f3758116"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKk61QMnJ2rM7whpN19","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SShLo3Xskin3kHRSYNXeZC","name":"ToolSearch","input":{"query":"select:TodoWrite","max_results":1},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":33694,"cache_read_input_tokens":21718,"cache_creation":{"ephemeral_5m_input_tokens":33694,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a85e517b-db05-4901-b234-e9a3b7016b9a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01SShLo3Xskin3kHRSYNXeZC","content":[{"type":"tool_reference","tool_name":"TodoWrite"}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"814160c1-67e6-4d45-82c1-fe32fd803164","timestamp":"2026-08-24T21:59:19.025Z","tool_use_result":{"matches":["TodoWrite"],"query":"select:TodoWrite","total_deferred_tools":17}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"35c5cbad-6370-4d53-ad40-a71913a57e76","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKkW5zhHh7AeE6qsshG","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WAvoiMf3EGjf5a6CZUKXQF","name":"TodoWrite","input":{"todos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"in_progress","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"pending","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3582,"cache_read_input_tokens":55412,"cache_creation":{"ephemeral_5m_input_tokens":3582,"ephemeral_1h_input_tokens":0},"output_tokens":25,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2de20ff4-a7dd-4a47-af76-a3369e5c3f70"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WAvoiMf3EGjf5a6CZUKXQF","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0b311774-e9ce-4aec-894d-d9eb387fff58","timestamp":"2026-08-24T21:59:28.680Z","tool_use_result":{"oldTodos":[{"content":"Clé SerpApi dans .env + créer .env.example","status":"completed","activeForm":"Configurant SerpApi (.env / .env.example)"},{"content":"Migrations db.py : listing_events, buildings, building_key, managers, manager_reviews, recycled_cache","status":"completed","activeForm":"Ajoutant les migrations de schéma"},{"content":"Détection d'événements dans sync_source + module louka/history.py","status":"in_progress","activeForm":"Implémentant l'historique des annonces"},{"content":"Module louka/building.py — passeport immeuble, rotation, pression loyers","status":"pending","activeForm":"Implémentant le passeport immeuble"},{"content":"Module louka/recycled.py — détection annonces recyclées","status":"pending","activeForm":"Implémentant la détection d'annonces recyclées"},{"content":"Module louka/coutreel.py — coût réel mensuel + prix/pi² + percentile","status":"pending","activeForm":"Implémentant le coût réel mensuel"},{"content":"Module louka/hiver.py — score vie quotidienne en hiver","status":"pending","activeForm":"Implémentant le score hiver"},{"content":"Modules louka/serpapi_client.py + louka/managers.py — fiche gestionnaire + avis Google","status":"pending","activeForm":"Implémentant la fiche gestionnaire (SerpApi)"},{"content":"Endpoints API web.py + CLI run.py + hooks ingest.watch","status":"pending","activeForm":"Branchant API, CLI et jobs"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Construisant les composants frontend"},{"content":"Rollup initial buildings + tests curl des nouveaux endpoints","status":"pending","activeForm":"Exécutant rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Rebuildant et redéployant"},{"content":"Commit + push origin main (spbgit)","status":"pending","activeForm":"Committant et poussant"}],"newTodos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"in_progress","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"pending","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"bb264d1d-d7a4-4136-91d6-1af7c92cc8f0","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmDriWWpXn2RMWeNUK","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISmgQKpwEIERgCKkBLIsjI5/ai+LBnJqVsieL/ApAArIddM3Meg0FatYJQyw+7mBSdpY1oihXJxXtyEsNIp4eCq/XT3sR+cpsDC22mMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHG/bLUBrABAhIMqFi1kCSnR2fdEKyQGgzPyG0RtFB/UOsu7+0iMO4jlNXfpwh18uAIK5AVhsvuY8ORiNkf2yMUnF2DaByD2vOKDcgNAbS1GExj13N6KSqfAht2dg71ZkIiWv2LuTgUzx8ZBuG/1Mc7KuYWEedrWLoqoF9iWE4QNd0kZ9QIULjOIXAZ9Km6kx3rf0MNTyAfZ91T7L9twCPHKAm5pgmrL+PEHg+39+2EVJRYwpxIuHExc264Hapi/Fo2DkT+vaSMRXeECxrw3w7dBqyJCosl6xE1eOxSpHQCPV95KPpXeWLxGXP9Us4VgedxH2iycQ9KuQQoB28G45BGUX53D9XQS1JZdlvElFvH43+Yq5qjyHV9eth3t0A88u5kE9o6JZVJZoP5RM6q+5zoka6/hTz9rSrAVQwdYxWJ0y0+8GlwUiyuTvk+DSj7JxOPjwPgqcvULqnHrp6i3lyAHLCOnZxkCOrTLYxRQ9S55qYWJ1ZXzA62GAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":861,"cache_read_input_tokens":58994,"cache_creation":{"ephemeral_5m_input_tokens":861,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"08f7967b-1978-452d-a078-9c31434f7304"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmDriWWpXn2RMWeNUK","type":"message","role":"assistant","content":[{"type":"text","text":"J'écris maintenant les modules gestionnaire. Je vérifie d'abord le schéma exact des tables `managers`/`manager_reviews` dans db.py et la structure de sources.json."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":861,"cache_read_input_tokens":58994,"cache_creation":{"ephemeral_5m_input_tokens":861,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"df802457-329a-4bb2-b348-3ddb225a02cf"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmDriWWpXn2RMWeNUK","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015hWbcrbWXjjMCykvF36vhh","name":"Grep","input":{"pattern":"managers|manager_reviews|mreviews","path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","output_mode":"content","-C":20},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":861,"cache_read_input_tokens":58994,"cache_creation":{"ephemeral_5m_input_tokens":861,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2e0c8aa6-f501-4afe-a3c8-0af5b4ce93b5"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015hWbcrbWXjjMCykvF36vhh","type":"tool_result","content":"188-CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n189-\n190-CREATE TABLE IF NOT EXISTS buildings (\n191- bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n192- -- (dedup._parse_address) ou « geo:lat,lng »\n193- address TEXT, -- adresse d'affichage (la plus fréquente)\n194- city TEXT,\n195- lat REAL, lng REAL,\n196- stats TEXT, -- JSON : unités, annonces, loyers,\n197- -- rotation, pression, gestionnaires…\n198- version TEXT, -- version du calcul (building.py)\n199- computed_at REAL\n200-);\n201-\n202-CREATE TABLE IF NOT EXISTS recycled_cache (\n203- uid TEXT PRIMARY KEY, -- listings.uid\n204- matches TEXT, -- JSON : correspondances probables + signaux\n205- computed_at REAL\n206-);\n207-\n208:CREATE TABLE IF NOT EXISTS managers (\n209- id INTEGER PRIMARY KEY AUTOINCREMENT,\n210- source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n211- name TEXT, -- nom canonique\n212- aliases TEXT, -- JSON : variantes de nom\n213- website TEXT,\n214- phone TEXT,\n215: -- résolution Google Maps (SerpApi) — voir louka/managers.py\n216- gmaps_place_id TEXT,\n217- gmaps_data_id TEXT,\n218- gmaps_name TEXT,\n219- gmaps_address TEXT,\n220- gmaps_rating REAL,\n221- gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n222- match_confidence REAL, -- 0..1 (association refusée si trop faible)\n223- match_signals TEXT, -- JSON : signaux ayant fondé l'association\n224- resolved_at REAL,\n225- resolve_failed INTEGER DEFAULT 0,\n226- review_stats TEXT, -- JSON : distribution, tendance, thèmes\n227- last_synced_at REAL\n228-);\n229-\n230:CREATE TABLE IF NOT EXISTS manager_reviews (\n231- id INTEGER PRIMARY KEY AUTOINCREMENT,\n232- manager_id INTEGER NOT NULL,\n233- external_review_id TEXT, -- id Google (déduplication)\n234- source TEXT DEFAULT 'google_maps',\n235- rating REAL,\n236- text TEXT,\n237- published_at TEXT, -- ISO si dérivable\n238- relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n239- author_name TEXT,\n240- author_review_count INTEGER,\n241- owner_response TEXT,\n242- owner_response_date TEXT,\n243- source_url TEXT,\n244- fetched_at REAL,\n245- content_hash TEXT, -- déduplication de secours\n246: analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n247-);\n248:CREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\n249:CREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n250: ON manager_reviews(manager_id, external_review_id);\n251-\"\"\"\n252-\n253-# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n254-_MIGRATIONS = {\n255- \"listings\": {\n256- \"availability_date\": \"TEXT\",\n257- \"area_sqft\": \"REAL\",\n258- \"pets\": \"TEXT\",\n259- \"furnished\": \"INTEGER\",\n260- \"details\": \"TEXT\",\n261- \"geocode_failed\": \"INTEGER DEFAULT 0\",\n262- \"miss_count\": \"INTEGER DEFAULT 0\",\n263- \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n264- \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n265- \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n266- \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n267- \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n268- \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n269- \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n270- \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c545af5a-0288-4f51-8805-60a4b27294a9","timestamp":"2026-08-24T21:59:35.516Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"188-CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n189-\n190-CREATE TABLE IF NOT EXISTS buildings (\n191- bkey TEXT PRIMARY KEY, -- clé d'adresse « civique|rue|ville »\n192- -- (dedup._parse_address) ou « geo:lat,lng »\n193- address TEXT, -- adresse d'affichage (la plus fréquente)\n194- city TEXT,\n195- lat REAL, lng REAL,\n196- stats TEXT, -- JSON : unités, annonces, loyers,\n197- -- rotation, pression, gestionnaires…\n198- version TEXT, -- version du calcul (building.py)\n199- computed_at REAL\n200-);\n201-\n202-CREATE TABLE IF NOT EXISTS recycled_cache (\n203- uid TEXT PRIMARY KEY, -- listings.uid\n204- matches TEXT, -- JSON : correspondances probables + signaux\n205- computed_at REAL\n206-);\n207-\n208:CREATE TABLE IF NOT EXISTS managers (\n209- id INTEGER PRIMARY KEY AUTOINCREMENT,\n210- source_id TEXT UNIQUE, -- id de la source (data/sources.json)\n211- name TEXT, -- nom canonique\n212- aliases TEXT, -- JSON : variantes de nom\n213- website TEXT,\n214- phone TEXT,\n215: -- résolution Google Maps (SerpApi) — voir louka/managers.py\n216- gmaps_place_id TEXT,\n217- gmaps_data_id TEXT,\n218- gmaps_name TEXT,\n219- gmaps_address TEXT,\n220- gmaps_rating REAL,\n221- gmaps_reviews INTEGER, -- nombre d'avis annoncé par Google\n222- match_confidence REAL, -- 0..1 (association refusée si trop faible)\n223- match_signals TEXT, -- JSON : signaux ayant fondé l'association\n224- resolved_at REAL,\n225- resolve_failed INTEGER DEFAULT 0,\n226- review_stats TEXT, -- JSON : distribution, tendance, thèmes\n227- last_synced_at REAL\n228-);\n229-\n230:CREATE TABLE IF NOT EXISTS manager_reviews (\n231- id INTEGER PRIMARY KEY AUTOINCREMENT,\n232- manager_id INTEGER NOT NULL,\n233- external_review_id TEXT, -- id Google (déduplication)\n234- source TEXT DEFAULT 'google_maps',\n235- rating REAL,\n236- text TEXT,\n237- published_at TEXT, -- ISO si dérivable\n238- relative_date_raw TEXT, -- « il y a 3 mois » (texte source)\n239- author_name TEXT,\n240- author_review_count INTEGER,\n241- owner_response TEXT,\n242- owner_response_date TEXT,\n243- source_url TEXT,\n244- fetched_at REAL,\n245- content_hash TEXT, -- déduplication de secours\n246: analysis TEXT -- JSON : topics/sentiment/severity (managers.py)\n247-);\n248:CREATE INDEX IF NOT EXISTS idx_mreviews_mgr ON manager_reviews(manager_id);\n249:CREATE UNIQUE INDEX IF NOT EXISTS idx_mreviews_ext\n250: ON manager_reviews(manager_id, external_review_id);\n251-\"\"\"\n252-\n253-# Colonnes ajoutées après la v1 — migration automatique des bases existantes.\n254-_MIGRATIONS = {\n255- \"listings\": {\n256- \"availability_date\": \"TEXT\",\n257- \"area_sqft\": \"REAL\",\n258- \"pets\": \"TEXT\",\n259- \"furnished\": \"INTEGER\",\n260- \"details\": \"TEXT\",\n261- \"geocode_failed\": \"INTEGER DEFAULT 0\",\n262- \"miss_count\": \"INTEGER DEFAULT 0\",\n263- \"dauid\": \"TEXT\", # aire de diffusion 2021 (stats de quartier)\n264- \"digest\": \"TEXT\", # JSON louka/textmine.py (description structurée)\n265- \"dup_of\": \"TEXT\", # uid de l'annonce canonique si doublon inter-sources\n266- \"dup_sources\": \"TEXT\", # JSON : autres sources où l'annonce est publiée\n267- \"bedrooms\": \"REAL\", # chambres fermées (convention QC : 4½ = 2)\n268- \"bathrooms\": \"REAL\", # salles de bain (1.5 = salle d'eau en plus)\n269- \"completeness\": \"REAL\", # score de complétude 0–100 (quality.py)\n270- \"published\": \"INTEGER DEFAULT 1\", # 0 = quarantaine (sous le seuil)","numLines":83}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmDriWWpXn2RMWeNUK","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HBUrQcvqEkR2D92TYX9ocb","name":"Bash","input":{"command":"python3 -c \"\nimport json\nd = json.load(open('/Users/simon-pierreboucher/apps/lou-ka/data/sources.json'))\nsrcs = d['sources']\nprint(len(srcs))\nprint(json.dumps(srcs[0], ensure_ascii=False, indent=1))\nfrom collections import Counter\nprint(Counter(s.get('status') for s in srcs))\nprint(Counter(k for s in srcs for k in s))\n\"","description":"Inspect sources.json structure"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":861,"cache_read_input_tokens":58994,"cache_creation":{"ephemeral_5m_input_tokens":861,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4e1880b3-6d29-4b3a-afec-67ae282c909d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HBUrQcvqEkR2D92TYX9ocb","type":"tool_result","content":"281\n{\n \"id\": \"lafrance_mathieu\",\n \"name\": \"Lafrance & Mathieu\",\n \"url\": \"https://lafrance-mathieu.com\",\n \"listing_url\": \"https://lafrance-mathieu.com/louer-appartement-quebec\",\n \"sectors\": \"Beauport, Charlesbourg, Limoilou, St-Roch, Lévis, Val-Bélair, Loretteville, L'Ancienne-Lorette, St-Augustin\",\n \"connector\": \"lafrance_mathieu\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n}\nCounter({'actif': 200, 'suspendu — widget RealVuu (app.realvuu.com) hors-ligne depuis 2026-08-10 (DNS NXDOMAIN) ; connecteur désactivé (disabled=True) et annonces archivées le 2026-08-18': 2, \"actif (dégradé) — le pare-feu de faubourgdumoulin.ca bloque l'IP du serveur Lou-Ka depuis ~2026-08-17 : repli automatique sur proxy Scrapfly (timeout direct 8 s)\": 1, 'non connectable — aucun prix/disponibilité publié sur le site': 1, 'couvert via lafrance_mathieu': 1, 'non connectable — aucune unité disponible affichée': 1, 'non connectable — hillpark.ca est un site placeholder vide': 1, 'non connectable — unités sans prix (« Contactez-nous »), Belle-Dame 100% loué': 1, 'non connectable — aucune annonce résidentielle locative actuellement': 1, \"actif — inventaire à 0 le 2026-08-09 : phases 1 et 3 complètes et les 60 unités de la phase 2 sont toutes « Louée » (vérifié par le REST unité par unité) ; le connecteur produira des annonces dès qu'une unité redeviendra disponible\": 1, \"actif — inventaire à 0 le 2026-08-09 (parc plein, le site ne rend des cartes que pour les logements libres) ; fixture rejouant l'état réel du 2023-02-24 via la Wayback Machine\": 1, \"non connectable actuellement — le sélecteur de plans d'Espace Centro marque les 128 unités « rented » (0 disponible) ; les prix « à partir de » par type sont du marketing statique sans dispo ; re-vérifier en période de location (le connecteur vaudrait la peine si des unités se libèrent)\": 1, \"non connectable — 0 logement résidentiel publié : la page /logements-a-louer/ (blocs texte manuels, thème WPResidence) n'affiche qu'une annonce de bureaux commerciaux (exclue)\": 1, \"actif — annonces « catalogue » (1 par immeuble) : la source ne publie ni prix, ni disponibilités, ni liste d'unités (location par téléphone/formulaire)\": 1, \"non connectable actuellement — plateforme « plogg immobilier » : les tableaux d'appartements des 2 immeubles affichent 100% COMPLET / PAS DISPONIBLE, aucun loyer publié (colonne $ vide) et la recherche (form-manager.php) renvoie 403 ; re-vérifier quand des unités se libèrent\": 1, \"non connectable — page « Appartements à louer » en texte libre affichant « Aucun appartement disponible pour le moment » depuis juillet 2020 (dernière mise à jour : 19 février 2024) ; aucun format d'annonce structuré observable, y compris dans tous les instantanés Wayback 2021-2024 — impossible d'écrire un parseur fiable. À re-vérifier périodiquement (acteur dominant du long terme à Gaspé)\": 1, 'actif — parc quasi plein : 1 unité libre sur 81 le 2026-08-09, volume normalement faible': 1, \"non connectable — aucune annonce résidentielle de marché : l'unique propriété « à louer » du site (constante de 2021 à 2026 d'après la Wayback Machine) est le Village Liberté, une résidence pour retraités avec services (repas, surveillance médicale) hors périmètre Lou-Ka, publiée sans prix ni unités ; les catégories condos/maisons à louer sont vides et le reste du catalogue est commercial ou à vendre. À re-vérifier périodiquement\": 1, \"non connectable — fiches immeubles de type brochure : le filtre ?type=a-louer ne filtre rien côté serveur (8 immeubles identiques), aucune disponibilité, aucun prix, aucun type d'unité structuré, CTA « Nous contacter » sans lien ; rien d'exploitable sans inventer\": 1, \"non connectable — 0 annonce publiée sur le site (le moteur de recherche interne, post_type locator, retourne « Aucun résultat disponible ») ; le lien « Appartements à louer » redirige vers Facebook Marketplace ; les pages d'immeubles ne publient ni prix ni vacances\": 1, 'non connectable — le corporatif et les 10 pages portfolio sont des brochures de projets sans disponibilités ni prix ; le microsite levivosherbrooke.com vérifié est aussi une vitrine sans inventaire ; re-vérifier si un module de disponibilités apparaît': 1, \"non connectable — le site n'affiche plus d'inventaire (ancienne page d'annonces en 404, sitemap property réduit à une fiche « test ») ; le portail actif du groupe est agencedelocationsherbrooke.com, couvert par le connecteur agence_sherbrooke\": 1, 'actif — 1 immeuble disponible le 2026-08-09 ; parc quasi plein, volume normalement faible': 1, \"non connectable — la page /a-louer/ ne pointe que vers 3 billets WordPress de mars 2023 (St-Jérôme, Blainville, Mirabel) contenant seulement secteur + téléphone : aucun prix, type d'unité, disponibilité ni annonce par logement (pages EN identiques)\": 1, \"non connectable actuellement — même plateforme « plogg immobilier » que Gestion A. Godbout : les 8 pages immeuble ont des tableaux d'appartements VIDES (0 ligne), aucun prix publié, et le POST du formulaire de recherche renvoie 403 ; re-vérifier périodiquement\": 1, \"non connectable — fiches immeubles de type brochure (rue Larouche, rue Maclaren…) : description du bâtiment et typologies « 1cc/2cc/3cc » mais aucune unité disponible, aucun loyer ni disponibilité ; parc communautaire (programme Accès-Logis SHQ) attribué par demande, pas d'annonces de marché\": 1, 'actif — très petit parc (3 fiches le 2026-08-09), mais fiches complètes (loyer, dispo, chambres, surface, photos)': 1, \"non connectable — gros parc en gestion (Desrosiers 192 log., Nuvo 504, Oasis…) mais AUCUNE annonce publique : les pages projets sont des ancres marketing sans prix ni disponibilité, seul un formulaire « demande de location » existe ; garder en veille, gros gain potentiel si un inventaire s'ouvre\": 1, 'actif — inventaire des unités disponibles via le portail Proprio Expert (Magex) ; distinct du connecteur heafey (Resimo, We 2/We 3/Loggia)': 1, \"non connectable — site vitrine sans inventaire : la page Lib Aylmer annonce « 160 condos locatifs, 2½ au 5½, occupation immédiate » et une promo « 2 mois gratuits », mais ne publie ni prix, ni liste d'unités, ni disponibilités (location par formulaire/téléphone) ; idem pour les autres complexes Lib. EMD-Bâtimo est déjà couvert côté Équinoxe (equinoxe_batimo)\": 1, \"non connectable — site corporatif (projets, impact, expertises, certification BCorp) sans aucune section d'annonces résidentielles à louer : aucun prix, aucune unité, aucune page de location identifiable\": 1, 'non connectable — la page « Location de logements » ne contient aucun inventaire (texte marketing « centaines de portes » + bouton « Contactez-nous ») : aucun prix, aucune fiche, aucune annonce au 2026-08-09 ; à re-vérifier périodiquement': 1, \"non connectable — page « Trouver un logement » (filtres secteur/prix/espace + carte) sans aucune propriété publiée : l'endpoint AJAX du thème (admin-ajax, action filterEntreprises) retourne une liste vide (maxPages 0) et le CPT WordPress « proprietes » exposé en REST est vide ; rien à parser sans inventer. À re-vérifier périodiquement\": 1, \"non connectable — l'ancienne page /à-louer répond 404 (GoDaddy) et la page restante « À louer à Gatineau » est un texte purement SEO/marketing : aucune annonce, aucun prix, aucune fiche de logement\": 1, \"actif — petit parc : 2 unités libres sur 7 fiches le 2026-08-09 (les fiches Location.aspx sont des liens morts, tout vit sur l'accueil)\": 1, \"non connectable — la page « Choisissez votre appartement » se réduit au widget JS Smart Condo Plans (smartcondoplans.silo.immo, building « six80 ») : aucun prix ni unité dans le HTML, et l'API du widget (v2/building/get-building-data / get-building-units) répond 500 « Server Error » hors session navigateur (seul get-labels passe) ; il faudrait un navigateur headless pour un seul immeuble\": 1, \"non connectable — tourliv.com ne sert plus qu'une redirection JavaScript vers /lander (page de stationnement de domaine GoDaddy) : le site du projet (tour locative, livraison 2026) est hors ligne ; à re-vérifier à la livraison\": 1, 'non connectable — la section « À louer » ne publie que des locaux COMMERCIAUX du centre Mont-Valois (loyer « Sur demande ») et une page Chelsea de prévente figée en 2018-2019 (« Disponibilité: 2019 », « Images à venir », loyer « selon la demande ») ; la variante /a-louer-chelsea-2/ répond 403 ; aucune annonce résidentielle avec loyer/dispo': 1, 'actif — prix par unité non publié (seul un « à partir de 1195 $ » global en prose) ; vocation 55+ sans services de soins (pas une RPA), à re-trancher selon la politique éditoriale': 1, 'actif — prix et dates jamais publiés (unités libres = cartes non « Indisponible », superficie et photo réelles) ; 4 immeubles sur 5 COMPLET au 2026-08-09': 1, 'actif — petit parc (~5 unités) avec prix mensuels publiés (meublé/non meublé) ; dates de disponibilité des fiches parfois périmées (texte brut conservé)': 1, 'actif — annonces à granularité immeuble (badge « Disponible », description, caractéristiques, photos) ; prix jamais publiés': 1, 'actif — annonces « catalogue » (1 par immeuble : adresse + photo) : la source ne publie ni prix, ni disponibilités, ni typologies (location par téléphone)': 1, 'actif — 1 annonce « catalogue » à granularité projet (type 4½, à partir de 1575 $, caractéristiques, photos) ; disponibilités par formulaire seulement': 1, \"actif — 4 annonces « catalogue » à granularité projet (description, commodités, photos) : la source ne publie ni prix, ni disponibilités, ni fiches d'unités (visites par formulaire)\": 1, \"non connectable — hors périmètre long terme : spécialiste du meublé tout inclus dont les boutons « Réserver En Ligne » pointent vers des fiches Airbnb (fr.airbnb.ca/rooms/…) = location courte durée ; le sitemap ne contient que 3 pages catégories (maisons-villas, studios-condos, appartements meublés) sans fiche d'annonce long terme structurée (vérifié 2026-08-09). À re-vérifier si une offre 12 mois structurée apparaît\": 1, \"non connectable — le site Wix propre est une vitrine sans aucune fiche d'annonce : présentation des 4 secteurs, brochure PDF à télécharger et formulaire de contact seulement (vérifié 2026-08-09) ; les annonces réelles passent par lokia.ca (chaîne de résidences RPA multi-régions, exclue du périmètre) et Facebook. À re-vérifier si le site publie un jour ses propres fiches\": 1, \"non connectable — vitrine Webflow sans aucune annonce résidentielle : le site ne détaille que la location d'entrepôts à Saint-Bruno et des compteurs marketing (« +80 locations d'appartements », « 150 locations d'entrepôts ») ; aucune page, fiche ou liste de logements à louer (vérifié 2026-08-09). Parc réel important — à re-vérifier périodiquement\": 1, \"non connectable — page unique statique (11,5 Ko, jQuery/Cloudflare) qui se contente d'énumérer 13 adresses en texte : aucune fiche par immeuble, aucun prix, aucune disponibilité, aucune photo attribuable à une adresse (« Je recherche un logement » = formulaire) — rien à parser en annonces, même au sens catalogue (vérifié 2026-08-09). À re-vérifier périodiquement ; distinct de Beaulieu Groupe Immobilier (Rimouski, connecteur beaulieu_groupe) et des Immeubles Beaulieu et Collin (immeubles_bc)\": 1, \"non connectable — site statique artisanal (Dreamweaver) : la liste des immeubles est intégralement marquée « COMPLET » (parc plein permanent) et l'état « disponible » n'a JAMAIS été observé (l'instantané Wayback 2016 n'affichait aucun statut ; aucune autre archive de la page liste) — impossible d'écrire un parseur de disponibilités véridique ni d'enregistrer une fixture non vide ; l'URL de la page change en outre chaque année (logements2025.html) et certains immeubles relèvent de coops/60+ à critères d'attribution. Les fiches (types + prix 623-635 $) sont du catalogue d'immeubles complets. À re-vérifier périodiquement (vérifié 2026-08-09)\": 1, \"non connectable — site statique HTML artisanal : pages descriptives par secteur (prose marketing + carrousels de photos + Street View), AUCUNE liste d'unités, aucun prix, aucune disponibilité, pas de robots.txt ni de structure d'annonce (vérifié 2026-08-09) ; les annonces réelles passent par LesPAC/téléphone. Un des plus gros proprios traditionnels de Sainte-Marie : bon candidat partenariat direct plutôt que scraping ; à re-vérifier périodiquement\": 1, \"non connectable — inventaire vide : la section « À LOUER » du site Wix affiche « Nouvelles offres à venir prochainement » sans aucune fiche reliée ; les pages fiches restantes du sitemap (à-louer-2…7-la-traversée, à-louer-logement-montmagny, copie-de-place-rachel-…) sont des VESTIGES ORPHELINS datés « DISPONIBLE LE 1ER JUILLET 2021 » (contenu dupliqué, non reliés à la navigation, sitemap figé au 2025-02-04) — rien d'actuel à parser (vérifié 2026-08-09). Adossé à un constructeur régional : à re-vérifier quand les projets Montmagny/Lévis livreront\": 1, 'non connectable — site toujours HORS LIGNE : lmgestionimmobiliere.com et www. renvoient 404 « ConnectYourDomain Error | Wix.com » (domaine Wix déconnecté), aucune annonce accessible — re-vérifié le 2026-08-09 (état identique au recensement du 2026-08-08). Gros parc : re-vérifier périodiquement, un connecteur se justifierait si le site revient en vie': 1, \"non connectable — re-vérifié le 2026-08-09 : certificat TLS TOUJOURS expiré (SSL certificate problem: certificate has expired) ; en ignorant le TLS, le site (GoDaddy Website Builder, one-pager) ne contient que présentation, galerie de photos, formulaire de contact et liens Facebook (« Les Résidences du Campus ») — AUCUNE section d'annonces ni fiche de logement ; les disponibilités passent par Facebook/téléphone. Gros parc : re-vérifier périodiquement, un connecteur se justifierait si une section d'annonces apparaît\": 1, \"non connectable — la page « Logements à louer » (GoDaddy Website Builder) ne contient aucune annonce : elle renvoie explicitement vers la page Facebook de l'entreprise (« Veuillez vous rendre sur notre page Facebook pour voir tous nos logements disponibles ») ; scraper Facebook est hors des pratiques Lou-Ka. À re-vérifier si un portail web apparaît\": 1, \"non connectable — la page « Logements à louer » (builder « Outil de création de site ») liste les immeubles avec des tableaux Prix/Disponibilité dont toutes les cases sont « COMPLET » et les prix vides ($ sans montant) ; bandeau « Nous aurons très peu de logements à louer cette année… liste d'attente par texto au 819-698-1821 ». Aucune annonce exploitable ni format structuré à parser. À re-vérifier périodiquement\": 1, \"non connectable — le portail /louer (site custom léger, boîtes .location-box propres et parsables) n'affiche que 4 fiches, toutes marquées « Complet », dont 2 chambres à Plessisville (575 $/525 $) et 2 logements 4½ à Saint-Ferdinand (975 $) ; aucune annonce disponible. Techniquement connectable en 30 min si l'inventaire se libère — à re-vérifier périodiquement\": 1, \"non connectable — site Wix vitrine : les pages « logements à louer plans » ne contiennent que des plans et photos des 2 immeubles, sans liste d'unités, sans prix ni disponibilités ; les seules mentions (« 3 1/2 libre dès maintenant », « À partir de 880 $ par mois ») sont dans une bannière d'accueil en image/texte marketing non daté. Rien de structuré à parser. À re-vérifier périodiquement\": 1, \"non connectable — site promotionnel Webflow mono-projet : aucune annonce d'unité ; la phase 2 publie seulement des prix « à partir de » par typologie (3½ 1 250 $, 4½ 1 695 $, 5½ 1 875 $) et « DISPONIBLE FÉVRIER-MARS 2026 », la phase 1 rien du tout — pas d'inventaire, pas d'external_id possible, disponibilité invérifiable. À re-vérifier si un portail d'unités apparaît\": 1, \"non connectable — aucun condo annoncé : le site l'écrit lui-même (« il n'y a actuellement aucun condo annoncé — ça pourrait être en janvier-février ») et fonctionne par formulaire d'intérêt/réservation ; de plus le parc Rocher 1-2 est un produit 55+/retraités (« unités parfaites pour des gens à la retraite ») en marge du périmètre Lou-Ka, et Le Rivya n'ouvre qu'en 2028. À re-vérifier périodiquement\": 1, \"non connectable — catalogue Avada (post cards Fusion) essentiellement « For Sale » (immeubles à vendre) ; côté location, 1-2 cartes « For Rent » sans page détail, aux champs vides (« -- Nb Chambre · -- Pi2 · $Contactez-nous »), sans type d'unité ni disponibilité, dans un markup Fusion non sémantique (divs de titres imbriqués sans classes de données). Rien d'assez fiable pour un parseur. À re-vérifier périodiquement\": 1, \"non connectable — répertoire d'immeubles Wix sans annonces d'unités : la page « appartements à louer » liste les immeubles et chaque fiche immeuble est du marketing statique (« Location disponible maintenant ! », inclusions) sans prix, sans unités, sans dates — aucun prix publié nulle part sur le site et disponibilité non datée/invérifiable. À re-vérifier si un portail d'unités apparaît\": 1, \"non connectable — site Wix : la page /logements-a-louer référencée au menu répond 404 (page retirée) et les 4 pages d'immeubles restantes sont descriptives, sans unité, prix ni disponibilité ; re-vérifier si la page des logements revient\": 1, \"non connectable — site Wix sans module d'annonces : l'unique offre courante (« À louer - 593 Ave des Érables… 1 500$/mois ») est un paragraphe de texte riche inséré à la main dans la page vitrine /realisation, sans identifiant, structure ni page dédiée — trop fragile et non suivable de façon fiable ; re-vérifier si un vrai répéteur d'annonces apparaît\": 1, \"non connectable — one-page WP 5.3/WPBakery : 12 cartes d'immeubles avec badges « À louer »/« Loué » mais en fourchettes de bâtiment (« 1200 à 1400 pi², 2 à 3 chambres »), sans prix, sans date, et liens des cartes morts (href=#) ; pas d'unités réelles exploitables\": 1, \"non connectable actuellement — l'accueil liste bien les immeubles mais chacun est marqué « Complet — Informez-vous pour les prochaines disponibilités » (aucun logement, prix ni date publiés ; consigne d'appeler le 450 522-4907) ; re-vérifier quand des unités se libèrent\": 1, \"non connectable — thème RealHomes avec seulement 3 fiches vitrines « logement à louer » (78 rue Briand, 90 de l'Église…), toutes hors zone Granby (Longueuil/Greenfield Park) et sans prix ni disponibilité, ni sur l'archive ni sur les fiches ; portefeuille surtout commercial\": 1, \"non connectable — le site s'est réorienté en offre de services de gestion POUR propriétaires (perception des loyers, baux, Tribunal administratif du logement…) : /nos-immeubles/ est une page de services avec formulaire de soumission, aucune annonce de logement\": 1, \"non connectable — « Nos immeubles » (4 pages paginées) et les fiches d'immeuble sont des présentations de bâtiments : aucune unité, aucun prix, aucune disponibilité sur l'archive ni sur les fiches sondées\": 1, 'non connectable — site statique ColdFusion : pages brochurables par secteur (Boisé Émile-Zola, Boisé Urbain…) avec visites virtuelles, mais aucune unité, aucun prix, aucune disponibilité ; location au téléphone (819 578-1490)': 1, 'non connectable — site vitrine Divi : la page « Location de logements » est un texte de présentation (50+) avec galerie photos ; aucune unité listée, aucun prix, aucune disponibilité, pas de CPT immobilier': 1, \"non connectable — arborescence SEO par secteur/grandeur et fiches property/ d'immeubles (Le 459, Jardins du Parc…) purement descriptives ; « Rechercher un appartement » n'est qu'un filtre par grandeur sans résultats datés : aucun prix ni disponibilité nulle part\": 1, 'non connectable — pages « Immeubles locatifs » / « Locatif Granby » / « Locatif Bromont » rédactionnelles (description des quartiers et des jumelés haut de gamme) : aucune annonce, aucun prix, aucune disponibilité datée': 1, 'non connectable — vitrine one-page de promoteur (prestige 55+) : blocs « VOIR LE PROJET » et texte « Pourquoi louer ? », aucune annonce, aucun prix, aucune disponibilité publiée': 1, 'non connectable — page « Immeubles résidentiels locatifs » (CMS Made Simple, © 2012) : un paragraphe de présentation et 3 photos ; aucune unité, aucun prix, aucune disponibilité, contact pour « planifier une visite »': 1, \"non connectable — vitrine WordPress d'un immeuble unique : pages /condos/ et /plans/ purement descriptives, aucune unité, aucun prix, aucune disponibilité (les seules mentions « disponible » du HTML sont des chaînes i18n de l'éditeur WP)\": 1, \"non connectable — site Duda dont le « catalogue » de propriétés est une boutique JS (MAIN_STORE) chargée côté client, avec prix explicitement masqués (data-product-list-price-behavior=hide) ; aucune annonce, prix ni disponibilité en SSR, la page /location n'offre qu'un formulaire de demande d'information\": 1, 'non connectable — site vitrine marketing : pages « Les unités » et « Plans | Prix » purement descriptives (visite guidée, plans en images), aucun prix, aucune unité datée, aucun module de disponibilité': 1, 'non connectable — archive Opal Estate (WP 5.5) de 167 fiches figées : prix uniquement en fiche détail et périmés (« 3 1/2 … 550.00$/mois », « LIBRE AU 1ER JUILLET 2021 »), aucune date de disponibilité maintenue, étiquette « trop tard déjà loué » posée sur une poignée de fiches seulement — inventaire non entretenu depuis ~2021': 1, 'non connectable actuellement — inventaire Livya complet (361 unités disponibles, GPS/pièces/pi²) mais AUCUN loyer publié (rentalPrice=0 et startingAtRentalPrice=0 sur toutes les unités, vérifié 2026-08-18) ; le patron _livya.py (voir atimco) se branche en 10 lignes si Cloria publie ses prix': 1, 'couvert via zumper': 1})\nCounter({'id': 281, 'name': 281, 'url': 281, 'listing_url': 281, 'sectors': 281, 'status': 281, 'region': 281, 'connector': 275})","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1a920ffe-cb0d-48a7-a1b1-0cb0a90d621b","timestamp":"2026-08-24T21:59:36.887Z","tool_use_result":{"stdout":"281\n{\n \"id\": \"lafrance_mathieu\",\n \"name\": \"Lafrance & Mathieu\",\n \"url\": \"https://lafrance-mathieu.com\",\n \"listing_url\": \"https://lafrance-mathieu.com/louer-appartement-quebec\",\n \"sectors\": \"Beauport, Charlesbourg, Limoilou, St-Roch, Lévis, Val-Bélair, Loretteville, L'Ancienne-Lorette, St-Augustin\",\n \"connector\": \"lafrance_mathieu\",\n \"status\": \"actif\",\n \"region\": \"Québec\"\n}\nCounter({'actif': 200, 'suspendu — widget RealVuu (app.realvuu.com) hors-ligne depuis 2026-08-10 (DNS NXDOMAIN) ; connecteur désactivé (disabled=True) et annonces archivées le 2026-08-18': 2, \"actif (dégradé) — le pare-feu de faubourgdumoulin.ca bloque l'IP du serveur Lou-Ka depuis ~2026-08-17 : repli automatique sur proxy Scrapfly (timeout direct 8 s)\": 1, 'non connectable — aucun prix/disponibilité publié sur le site': 1, 'couvert via lafrance_mathieu': 1, 'non connectable — aucune unité disponible affichée': 1, 'non connectable — hillpark.ca est un site placeholder vide': 1, 'non connectable — unités sans prix (« Contactez-nous »), Belle-Dame 100% loué': 1, 'non connectable — aucune annonce résidentielle locative actuellement': 1, \"actif — inventaire à 0 le 2026-08-09 : phases 1 et 3 complètes et les 60 unités de la phase 2 sont toutes « Louée » (vérifié par le REST unité par unité) ; le connecteur produira des annonces dès qu'une unité redeviendra disponible\": 1, \"actif — inventaire à 0 le 2026-08-09 (parc plein, le site ne rend des cartes que pour les logements libres) ; fixture rejouant l'état réel du 2023-02-24 via la Wayback Machine\": 1, \"non connectable actuellement — le sélecteur de plans d'Espace Centro marque les 128 unités « rented » (0 disponible) ; les prix « à partir de » par type sont du marketing statique sans dispo ; re-vérifier en période de location (le connecteur vaudrait la peine si des unités se libèrent)\": 1, \"non connectable — 0 logement résidentiel publié : la page /logements-a-louer/ (blocs texte manuels, thème WPResidence) n'affiche qu'une annonce de bureaux commerciaux (exclue)\": 1, \"actif — annonces « catalogue » (1 par immeuble) : la source ne publie ni prix, ni disponibilités, ni liste d'unités (location par téléphone/formulaire)\": 1, \"non connectable actuellement — plateforme « plogg immobilier » : les tableaux d'appartements des 2 immeubles affichent 100% COMPLET / PAS DISPONIBLE, aucun loyer publié (colonne $ vide) et la recherche (form-manager.php) renvoie 403 ; re-vérifier quand des unités se libèrent\": 1, \"non connectable — page « Appartements à louer » en texte libre affichant « Aucun appartement disponible pour le moment » depuis juillet 2020 (dernière mise à jour : 19 février 2024) ; aucun format d'annonce structuré observable, y compris dans tous les instantanés Wayback 2021-2024 — impossible d'écrire un parseur fiable. À re-vérifier périodiquement (acteur dominant du long terme à Gaspé)\": 1, 'actif — parc quasi plein : 1 unité libre sur 81 le 2026-08-09, volume normalement faible': 1, \"non connectable — aucune annonce résidentielle de marché : l'unique propriété « à louer » du site (constante de 2021 à 2026 d'après la Wayback Machine) est le Village Liberté, une résidence pour retraités avec services (repas, surveillance médicale) hors périmètre Lou-Ka, publiée sans prix ni unités ; les catégories condos/maisons à louer sont vides et le reste du catalogue est commercial ou à vendre. À re-vérifier périodiquement\": 1, \"non connectable — fiches immeubles de type brochure : le filtre ?type=a-louer ne filtre rien côté serveur (8 immeubles identiques), aucune disponibilité, aucun prix, aucun type d'unité structuré, CTA « Nous contacter » sans lien ; rien d'exploitable sans inventer\": 1, \"non connectable — 0 annonce publiée sur le site (le moteur de recherche interne, post_type locator, retourne « Aucun résultat disponible ») ; le lien « Appartements à louer » redirige vers Facebook Marketplace ; les pages d'immeubles ne publient ni prix ni vacances\": 1, 'non connectable — le corporatif et les 10 pages portfolio sont des brochures de projets sans disponibilités ni prix ; le microsite levivosherbrooke.com vérifié est aussi une vitrine sans inventaire ; re-vérifier si un module de disponibilités apparaît': 1, \"non connectable — le site n'affiche plus d'inventaire (ancienne page d'annonces en 404, sitemap property réduit à une fiche « test ») ; le portail actif du groupe est agencedelocationsherbrooke.com, couvert par le connecteur agence_sherbrooke\": 1, 'actif — 1 immeuble disponible le 2026-08-09 ; parc quasi plein, volume normalement faible': 1, \"non connectable — la page /a-louer/ ne pointe que vers 3 billets WordPress de mars 2023 (St-Jérôme, Blainville, Mirabel) contenant seulement secteur + téléphone : aucun prix, type d'unité, disponibilité ni annonce par logement (pages EN identiques)\": 1, \"non connectable actuellement — même plateforme « plogg immobilier » que Gestion A. Godbout : les 8 pages immeuble ont des tableaux d'appartements VIDES (0 ligne), aucun prix publié, et le POST du formulaire de recherche renvoie 403 ; re-vérifier périodiquement\": 1, \"non connectable — fiches immeubles de type brochure (rue Larouche, rue Maclaren…) : description du bâtiment et typologies « 1cc/2cc/3cc » mais aucune unité disponible, aucun loyer ni disponibilité ; parc communautaire (programme Accès-Logis SHQ) attribué par demande, pas d'annonces de marché\": 1, 'actif — très petit parc (3 fiches le 2026-08-09), mais fiches complètes (loyer, dispo, chambres, surface, photos)': 1, \"non connectable — gros parc en gestion (Desrosiers 192 log., Nuvo 504, Oasis…) mais AUCUNE annonce publique : les pages projets sont des ancres marketing sans prix ni disponibilité, seul un formulaire « demande de location » existe ; garder en veille, gros gain potentiel si un inventaire s'ouvre\": 1, 'actif — inventaire des unités disponibles via le portail Proprio Expert (Magex) ; distinct du connecteur heafey (Resimo, We 2/We 3/Loggia)': 1, \"non connectable — site vitrine sans inventaire : la page Lib Aylmer annonce « 160 condos locatifs, 2½ au 5½, occupation immédiate » et une promo « 2 mois gratuits », mais ne publie ni prix, ni liste d'unités, ni disponibilités (location par formulaire/téléphone) ; idem pour les autres complexes Lib. EMD-Bâtimo est déjà couvert côté Équinoxe (equinoxe_batimo)\": 1, \"non connectable — site corporatif (projets, impact, expertises, certification BCorp) sans aucune section d'annonces résidentielles à louer : aucun prix, aucune unité, aucune page de location identifiable\": 1, 'non connectable — la page « Location de logements » ne contient aucun inventaire (texte marketing « centaines de portes » + bouton « Contactez-nous ») : aucun prix, aucune fiche, aucune annonce au 2026-08-09 ; à re-vérifier périodiquement': 1, \"non connectable — page « Trouver un logement » (filtres secteur/prix/espace + carte) sans aucune propriété publiée : l'endpoint AJAX du thème (admin-ajax, action filterEntreprises) retourne une liste vide (maxPages 0) et le CPT WordPress « proprietes » exposé en REST est vide ; rien à parser sans inventer. À re-vérifier périodiquement\": 1, \"non connectable — l'ancienne page /à-louer répond 404 (GoDaddy) et la page restante « À louer à Gatineau » est un texte purement SEO/marketing : aucune annonce, aucun prix, aucune fiche de logement\": 1, \"actif — petit parc : 2 unités libres sur 7 fiches le 2026-08-09 (les fiches Location.aspx sont des liens morts, tout vit sur l'accueil)\": 1, \"non connectable — la page « Choisissez votre appartement » se réduit au widget JS Smart Condo Plans (smartcondoplans.silo.immo, building « six80 ») : aucun prix ni unité dans le HTML, et l'API du widget (v2/building/get-building-data / get-building-units) répond 500 « Server Error » hors session navigateur (seul get-labels passe) ; il faudrait un navigateur headless pour un seul immeuble\": 1, \"non connectable — tourliv.com ne sert plus qu'une redirection JavaScript vers /lander (page de stationnement de domaine GoDaddy) : le site du projet (tour locative, livraison 2026) est hors ligne ; à re-vérifier à la livraison\": 1, 'non connectable — la section « À louer » ne publie que des locaux COMMERCIAUX du centre Mont-Valois (loyer « Sur demande ») et une page Chelsea de prévente figée en 2018-2019 (« Disponibilité: 2019 », « Images à venir », loyer « selon la demande ») ; la variante /a-louer-chelsea-2/ répond 403 ; aucune annonce résidentielle avec loyer/dispo': 1, 'actif — prix par unité non publié (seul un « à partir de 1195 $ » global en prose) ; vocation 55+ sans services de soins (pas une RPA), à re-trancher selon la politique éditoriale': 1, 'actif — prix et dates jamais publiés (unités libres = cartes non « Indisponible », superficie et photo réelles) ; 4 immeubles sur 5 COMPLET au 2026-08-09': 1, 'actif — petit parc (~5 unités) avec prix mensuels publiés (meublé/non meublé) ; dates de disponibilité des fiches parfois périmées (texte brut conservé)': 1, 'actif — annonces à granularité immeuble (badge « Disponible », description, caractéristiques, photos) ; prix jamais publiés': 1, 'actif — annonces « catalogue » (1 par immeuble : adresse + photo) : la source ne publie ni prix, ni disponibilités, ni typologies (location par téléphone)': 1, 'actif — 1 annonce « catalogue » à granularité projet (type 4½, à partir de 1575 $, caractéristiques, photos) ; disponibilités par formulaire seulement': 1, \"actif — 4 annonces « catalogue » à granularité projet (description, commodités, photos) : la source ne publie ni prix, ni disponibilités, ni fiches d'unités (visites par formulaire)\": 1, \"non connectable — hors périmètre long terme : spécialiste du meublé tout inclus dont les boutons « Réserver En Ligne » pointent vers des fiches Airbnb (fr.airbnb.ca/rooms/…) = location courte durée ; le sitemap ne contient que 3 pages catégories (maisons-villas, studios-condos, appartements meublés) sans fiche d'annonce long terme structurée (vérifié 2026-08-09). À re-vérifier si une offre 12 mois structurée apparaît\": 1, \"non connectable — le site Wix propre est une vitrine sans aucune fiche d'annonce : présentation des 4 secteurs, brochure PDF à télécharger et formulaire de contact seulement (vérifié 2026-08-09) ; les annonces réelles passent par lokia.ca (chaîne de résidences RPA multi-régions, exclue du périmètre) et Facebook. À re-vérifier si le site publie un jour ses propres fiches\": 1, \"non connectable — vitrine Webflow sans aucune annonce résidentielle : le site ne détaille que la location d'entrepôts à Saint-Bruno et des compteurs marketing (« +80 locations d'appartements », « 150 locations d'entrepôts ») ; aucune page, fiche ou liste de logements à louer (vérifié 2026-08-09). Parc réel important — à re-vérifier périodiquement\": 1, \"non connectable — page unique statique (11,5 Ko, jQuery/Cloudflare) qui se contente d'énumérer 13 adresses en texte : aucune fiche par immeuble, aucun prix, aucune disponibilité, aucune photo attribuable à une adresse (« Je recherche un logement » = formulaire) — rien à parser en annonces, même au sens catalogue (vérifié 2026-08-09). À re-vérifier périodiquement ; distinct de Beaulieu Groupe Immobilier (Rimouski, connecteur beaulieu_groupe) et des Immeubles Beaulieu et Collin (immeubles_bc)\": 1, \"non connectable — site statique artisanal (Dreamweaver) : la liste des immeubles est intégralement marquée « COMPLET » (parc plein permanent) et l'état « disponible » n'a JAMAIS été observé (l'instantané Wayback 2016 n'affichait aucun statut ; aucune autre archive de la page liste) — impossible d'écrire un parseur de disponibilités véridique ni d'enregistrer une fixture non vide ; l'URL de la page change en outre chaque année (logements2025.html) et certains immeubles relèvent de coops/60+ à critères d'attribution. Les fiches (types + prix 623-635 $) sont du catalogue d'immeubles complets. À re-vérifier périodiquement (vérifié 2026-08-09)\": 1, \"non connectable — site statique HTML artisanal : pages descriptives par secteur (prose marketing + carrousels de photos + Street View), AUCUNE liste d'unités, aucun prix, aucune disponibilité, pas de robots.txt ni de structure d'annonce (vérifié 2026-08-09) ; les annonces réelles passent par LesPAC/téléphone. Un des plus gros proprios traditionnels de Sainte-Marie : bon candidat partenariat direct plutôt que scraping ; à re-vérifier périodiquement\": 1, \"non connectable — inventaire vide : la section « À LOUER » du site Wix affiche « Nouvelles offres à venir prochainement » sans aucune fiche reliée ; les pages fiches restantes du sitemap (à-louer-2…7-la-traversée, à-louer-logement-montmagny, copie-de-place-rachel-…) sont des VESTIGES ORPHELINS datés « DISPONIBLE LE 1ER JUILLET 2021 » (contenu dupliqué, non reliés à la navigation, sitemap figé au 2025-02-04) — rien d'actuel à parser (vérifié 2026-08-09). Adossé à un constructeur régional : à re-vérifier quand les projets Montmagny/Lévis livreront\": 1, 'non connectable — site toujours HORS LIGNE : lmgestionimmobiliere.com et www. renvoient 404 « ConnectYourDomain Error | Wix.com » (domaine Wix déconnecté), aucune annonce accessible — re-vérifié le 2026-08-09 (état identique au recensement du 2026-08-08). Gros parc : re-vérifier périodiquement, un connecteur se justifierait si le site revient en vie': 1, \"non connectable — re-vérifié le 2026-08-09 : certificat TLS TOUJOURS expiré (SSL certificate problem: certificate has expired) ; en ignorant le TLS, le site (GoDaddy Website Builder, one-pager) ne contient que présentation, galerie de photos, formulaire de contact et liens Facebook (« Les Résidences du Campus ») — AUCUNE section d'annonces ni fiche de logement ; les disponibilités passent par Facebook/téléphone. Gros parc : re-vérifier périodiquement, un connecteur se justifierait si une section d'annonces apparaît\": 1, \"non connectable — la page « Logements à louer » (GoDaddy Website Builder) ne contient aucune annonce : elle renvoie explicitement vers la page Facebook de l'entreprise (« Veuillez vous rendre sur notre page Facebook pour voir tous nos logements disponibles ») ; scraper Facebook est hors des pratiques Lou-Ka. À re-vérifier si un portail web apparaît\": 1, \"non connectable — la page « Logements à louer » (builder « Outil de création de site ») liste les immeubles avec des tableaux Prix/Disponibilité dont toutes les cases sont « COMPLET » et les prix vides ($ sans montant) ; bandeau « Nous aurons très peu de logements à louer cette année… liste d'attente par texto au 819-698-1821 ». Aucune annonce exploitable ni format structuré à parser. À re-vérifier périodiquement\": 1, \"non connectable — le portail /louer (site custom léger, boîtes .location-box propres et parsables) n'affiche que 4 fiches, toutes marquées « Complet », dont 2 chambres à Plessisville (575 $/525 $) et 2 logements 4½ à Saint-Ferdinand (975 $) ; aucune annonce disponible. Techniquement connectable en 30 min si l'inventaire se libère — à re-vérifier périodiquement\": 1, \"non connectable — site Wix vitrine : les pages « logements à louer plans » ne contiennent que des plans et photos des 2 immeubles, sans liste d'unités, sans prix ni disponibilités ; les seules mentions (« 3 1/2 libre dès maintenant », « À partir de 880 $ par mois ») sont dans une bannière d'accueil en image/texte marketing non daté. Rien de structuré à parser. À re-vérifier périodiquement\": 1, \"non connectable — site promotionnel Webflow mono-projet : aucune annonce d'unité ; la phase 2 publie seulement des prix « à partir de » par typologie (3½ 1 250 $, 4½ 1 695 $, 5½ 1 875 $) et « DISPONIBLE FÉVRIER-MARS 2026 », la phase 1 rien du tout — pas d'inventaire, pas d'external_id possible, disponibilité invérifiable. À re-vérifier si un portail d'unités apparaît\": 1, \"non connectable — aucun condo annoncé : le site l'écrit lui-même (« il n'y a actuellement aucun condo annoncé — ça pourrait être en janvier-février ») et fonctionne par formulaire d'intérêt/réservation ; de plus le parc Rocher 1-2 est un produit 55+/retraités (« unités parfaites pour des gens à la retraite ») en marge du périmètre Lou-Ka, et Le Rivya n'ouvre qu'en 2028. À re-vérifier périodiquement\": 1, \"non connectable — catalogue Avada (post cards Fusion) essentiellement « For Sale » (immeubles à vendre) ; côté location, 1-2 cartes « For Rent » sans page détail, aux champs vides (« -- Nb Chambre · -- Pi2 · $Contactez-nous »), sans type d'unité ni disponibilité, dans un markup Fusion non sémantique (divs de titres imbriqués sans classes de données). Rien d'assez fiable pour un parseur. À re-vérifier périodiquement\": 1, \"non connectable — répertoire d'immeubles Wix sans annonces d'unités : la page « appartements à louer » liste les immeubles et chaque fiche immeuble est du marketing statique (« Location disponible maintenant ! », inclusions) sans prix, sans unités, sans dates — aucun prix publié nulle part sur le site et disponibilité non datée/invérifiable. À re-vérifier si un portail d'unités apparaît\": 1, \"non connectable — site Wix : la page /logements-a-louer référencée au menu répond 404 (page retirée) et les 4 pages d'immeubles restantes sont descriptives, sans unité, prix ni disponibilité ; re-vérifier si la page des logements revient\": 1, \"non connectable — site Wix sans module d'annonces : l'unique offre courante (« À louer - 593 Ave des Érables… 1 500$/mois ») est un paragraphe de texte riche inséré à la main dans la page vitrine /realisation, sans identifiant, structure ni page dédiée — trop fragile et non suivable de façon fiable ; re-vérifier si un vrai répéteur d'annonces apparaît\": 1, \"non connectable — one-page WP 5.3/WPBakery : 12 cartes d'immeubles avec badges « À louer »/« Loué » mais en fourchettes de bâtiment (« 1200 à 1400 pi², 2 à 3 chambres »), sans prix, sans date, et liens des cartes morts (href=#) ; pas d'unités réelles exploitables\": 1, \"non connectable actuellement — l'accueil liste bien les immeubles mais chacun est marqué « Complet — Informez-vous pour les prochaines disponibilités » (aucun logement, prix ni date publiés ; consigne d'appeler le 450 522-4907) ; re-vérifier quand des unités se libèrent\": 1, \"non connectable — thème RealHomes avec seulement 3 fiches vitrines « logement à louer » (78 rue Briand, 90 de l'Église…), toutes hors zone Granby (Longueuil/Greenfield Park) et sans prix ni disponibilité, ni sur l'archive ni sur les fiches ; portefeuille surtout commercial\": 1, \"non connectable — le site s'est réorienté en offre de services de gestion POUR propriétaires (perception des loyers, baux, Tribunal administratif du logement…) : /nos-immeubles/ est une page de services avec formulaire de soumission, aucune annonce de logement\": 1, \"non connectable — « Nos immeubles » (4 pages paginées) et les fiches d'immeuble sont des présentations de bâtiments : aucune unité, aucun prix, aucune disponibilité sur l'archive ni sur les fiches sondées\": 1, 'non connectable — site statique ColdFusion : pages brochurables par secteur (Boisé Émile-Zola, Boisé Urbain…) avec visites virtuelles, mais aucune unité, aucun prix, aucune disponibilité ; location au téléphone (819 578-1490)': 1, 'non connectable — site vitrine Divi : la page « Location de logements » est un texte de présentation (50+) avec galerie photos ; aucune unité listée, aucun prix, aucune disponibilité, pas de CPT immobilier': 1, \"non connectable — arborescence SEO par secteur/grandeur et fiches property/ d'immeubles (Le 459, Jardins du Parc…) purement descriptives ; « Rechercher un appartement » n'est qu'un filtre par grandeur sans résultats datés : aucun prix ni disponibilité nulle part\": 1, 'non connectable — pages « Immeubles locatifs » / « Locatif Granby » / « Locatif Bromont » rédactionnelles (description des quartiers et des jumelés haut de gamme) : aucune annonce, aucun prix, aucune disponibilité datée': 1, 'non connectable — vitrine one-page de promoteur (prestige 55+) : blocs « VOIR LE PROJET » et texte « Pourquoi louer ? », aucune annonce, aucun prix, aucune disponibilité publiée': 1, 'non connectable — page « Immeubles résidentiels locatifs » (CMS Made Simple, © 2012) : un paragraphe de présentation et 3 photos ; aucune unité, aucun prix, aucune disponibilité, contact pour « planifier une visite »': 1, \"non connectable — vitrine WordPress d'un immeuble unique : pages /condos/ et /plans/ purement descriptives, aucune unité, aucun prix, aucune disponibilité (les seules mentions « disponible » du HTML sont des chaînes i18n de l'éditeur WP)\": 1, \"non connectable — site Duda dont le « catalogue » de propriétés est une boutique JS (MAIN_STORE) chargée côté client, avec prix explicitement masqués (data-product-list-price-behavior=hide) ; aucune annonce, prix ni disponibilité en SSR, la page /location n'offre qu'un formulaire de demande d'information\": 1, 'non connectable — site vitrine marketing : pages « Les unités » et « Plans | Prix » purement descriptives (visite guidée, plans en images), aucun prix, aucune unité datée, aucun module de disponibilité': 1, 'non connectable — archive Opal Estate (WP 5.5) de 167 fiches figées : prix uniquement en fiche détail et périmés (« 3 1/2 … 550.00$/mois », « LIBRE AU 1ER JUILLET 2021 »), aucune date de disponibilité maintenue, étiquette « trop tard déjà loué » posée sur une poignée de fiches seulement — inventaire non entretenu depuis ~2021': 1, 'non connectable actuellement — inventaire Livya complet (361 unités disponibles, GPS/pièces/pi²) mais AUCUN loyer publié (rentalPrice=0 et startingAtRentalPrice=0 sur toutes les unités, vérifié 2026-08-18) ; le patron _livya.py (voir atimco) se branche en 10 lignes si Cloria publie ses prix': 1, 'couvert via zumper': 1})\nCounter({'id': 281, 'name': 281, 'url': 281, 'listing_url': 281, 'sectors': 281, 'status': 281, 'region': 281, 'connector': 275})","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"78feeb63-de31-4625-a4c7-a3792eab67eb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmpM8RgzRfQxhg6VrZ","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISiAgKpwEIERgCKkCRjO5GsB8f6ib117UWjXaMZuSkvHBcifN3qD37Ba/dVc6LH9UBz6j9X+ifv2PI/w3dAvJFuAlGZ5U5FMAt/ze3Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHQ/bLUBrABAhIMRtH6d4aUl6rYDE5BGgwIsDN7RKbkHpTID8siMILOemC5M1zcuVnLBC+k8SFOKNspZVSI20K1NHRMWrbgxtIQl8sOxJumCSUrh8AaKiqNBpw1HOU9ba++ld8VZcqusWKGFaY41mGf2DTt5UOaOI76ouPhh7PPwXBQ0h4kZcR2PX16P3TxXEdCZIG8m4mPUQR2winKER/rETO/OlsjOx0htJxvDI9S1+oWEknYrAtD/SGyAk2uvGggYAoVL1D6jTAq03BwL9XVry7A5Ij6Bz/TwYDLM6JWb9DRNVVOXKgo94K5Y9lXL/Qjfz+o7v/vXnhlmdWxsLKEFA66GljgErGdOoie+jjtnPExa6bvxHTzKJcA/e66Z5Tek6I43PVdFgri+rnK16Q7XfkXSu2SAb15LJjN14eNGh4u+N0/aVWDsvX8ph1EQAb7Z+iWY+c5Ovje+AmpdrpY8pHdSko+nLfx7kl1yRgQdSo4TX0OJd7S2C3n6V46uVFFXxB9WeKTLR+FchQWTWv4hodjzM1miPEJkbQpVOE2A9zLwO391V5X4cNfMZN3EROgi5YpeXEWefjkzg9ZSRUe0mNsP+vx2sDf9I9NXrnpS0RRwM/o8u3ub+fbiCJKXsMmSbUIR4JZ5O+kUNBbds/R7lRNe5vFGsrf7zOxYcbAZnZpdkbSuxZkQDOVRzAQqG18UBXr7J9VQAg6iK2sT/5a2/cq4t/dmFs1jHRBgPKKN1xKFeDEILlRnFrJNn5XmBEcyu3mS/BDZ3nRoVdfQCzLSSAkVLYIEdEs0DZB7Rw7h92oBlUvcpzjhXbi+oiRloPQuJCDNfOC+NFoYwxVXcAFcNVFmtZwro8H1ksYu/6hibHC0L7rEICPPkhxjTZmJ5In9ANDyViwJ6KolR4zqpwgLoDMR5UrGshKZ8wAQQx/7aPOk5hcQsO74VTDpVgqVFsGW+jZ87jjDlly2h7sfSuA42mRUeeuPVsKB7cOzpeuhJB8AFmaeuR0hKOhXBnrsyFYIE01G+zgFTj1ISDLeTzssWqIShDSQJE5vO/69McrmG5UcqfAgNcgf4B6lsGocyE5EmiY3Po15dB23UZ6mqVDItIIiwtlAQuv9jAEIttFrpjjZbJ53sN2Coxi8BGrfUvD1tL0YPsYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11740,"cache_read_input_tokens":59855,"cache_creation":{"ephemeral_5m_input_tokens":11740,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3fdf27a5-feda-4111-a2a4-fbb9f8f7de97"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmpM8RgzRfQxhg6VrZ","type":"message","role":"assistant","content":[{"type":"text","text":"281 sources confirmées. J'écris maintenant `serpapi_client.py` — je vérifie d'abord quelle lib HTTP le projet utilise."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11740,"cache_read_input_tokens":59855,"cache_creation":{"ephemeral_5m_input_tokens":11740,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"39c0c640-0cde-40b6-b7f8-acd821e29e63"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKmpM8RgzRfQxhg6VrZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01X4aSv2hFz1j4Uxsu8Pxh9F","name":"Grep","input":{"pattern":"^import requests|^import httpx|from curl_cffi|^import urllib","path":"/Users/simon-pierreboucher/apps/lou-ka/louka","output_mode":"content","head_limit":15},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11740,"cache_read_input_tokens":59855,"cache_creation":{"ephemeral_5m_input_tokens":11740,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d2c914af-992f-4c95-a43a-026f97a237db"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01X4aSv2hFz1j4Uxsu8Pxh9F","type":"tool_result","content":"louka/imgcheck.py:24:import requests\nlouka/poi.py:18:import requests\nlouka/hubfav.py:18:import requests\nlouka/tal.py:38:import urllib.parse\nlouka/tal.py:39:import urllib.request\nlouka/air.py:24:import urllib.request\nlouka/gaz.py:18:import urllib.request\nlouka/fixtures.py:17:import requests\nlouka/hubprofile.py:23:import requests\nlouka/rdl.py:25:import urllib.request\nlouka/geocode.py:17:import requests\nlouka/pdfgen.py:24:import requests\nlouka/commerces.py:20:import urllib.parse\nlouka/commerces.py:21:import urllib.request\nlouka/hydro.py:33:import urllib.parse\n\n[Showing results with pagination = limit: 15]"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ad1c2f1b-7b83-40f0-a0d6-a026c6422add","timestamp":"2026-08-24T21:59:45.869Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"louka/imgcheck.py:24:import requests\nlouka/poi.py:18:import requests\nlouka/hubfav.py:18:import requests\nlouka/tal.py:38:import urllib.parse\nlouka/tal.py:39:import urllib.request\nlouka/air.py:24:import urllib.request\nlouka/gaz.py:18:import urllib.request\nlouka/fixtures.py:17:import requests\nlouka/hubprofile.py:23:import requests\nlouka/rdl.py:25:import urllib.request\nlouka/geocode.py:17:import requests\nlouka/pdfgen.py:24:import requests\nlouka/commerces.py:20:import urllib.parse\nlouka/commerces.py:21:import urllib.request\nlouka/hydro.py:33:import urllib.parse","numLines":15,"appliedLimit":15}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"0666f500-47e4-4179-850a-a6a42c946913","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKnUsNpCJveT7rwMdnk","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIStAIKpwEIERgCKkCSY7I4C0Wx+XO0+S+S2fAlsNH7ywTmkyGrW44+ai6DBkNRN2sviJ6Fj7PJ5kg4trPIew5gNz0Wc1PCS1SPoIynMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHV/bLUBrABAhIMdGolId+d4qHGNbt8GgyEcRn1s9sxqOAcFm0iMBuxM5vETnx9DdnDfmIHL0s7fLD5Dn0gpImPtkNcf1DN+d8rUeE4hyH0lK4e2FI2Uyo6tznN9t+OX8cC7//yoO8qN0bMhJeQ9Q2PB+Qmg+a96ax+IaCTbTaovL/rQ88Sua/2BUSbIPyLcTrwHRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":785,"cache_read_input_tokens":71595,"cache_creation":{"ephemeral_5m_input_tokens":785,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c7cae7b7-3353-43f4-b057-7202a8ac28e9"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKnUsNpCJveT7rwMdnk","type":"message","role":"assistant","content":[{"type":"text","text":"`requests` est disponible. J'écris les deux modules."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":785,"cache_read_input_tokens":71595,"cache_creation":{"ephemeral_5m_input_tokens":785,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"deb17753-dc5f-4c03-adb6-b45a7190fa94"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKnUsNpCJveT7rwMdnk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_018pfCWgy7i1yV1rTnx1cG2t","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# serpapi_client.py : client SerpApi minimal, STRICTEMENT côté serveur.\n#\n# Sécurité de la clé (non négociable) :\n# - lue UNIQUEMENT depuis la variable d'environnement SERPAPI_API_KEY\n# (chargée par run.py depuis .env, jamais commitée — voir .env.example) ;\n# - jamais écrite dans les logs, les exceptions ni les réponses API ;\n# - aucune requête SerpApi ne part du navigateur : ce module n'est importé\n# que par les jobs serveur (managers.py).\n#\n# Robustesse : timeout 30 s, 3 tentatives avec repli exponentiel sur les\n# erreurs transitoires (réseau, 429, 5xx). Les erreurs définitives (4xx)\n# remontent immédiatement.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport os\nimport time\n\nimport requests\n\nBASE = \"https://serpapi.com/search.json\"\nTIMEOUT = 30\nRETRIES = 3\nBACKOFF = 2.0 # s — doublé à chaque tentative\n\n\nclass SerpApiError(RuntimeError):\n pass\n\n\ndef api_key() -> str | None:\n return os.environ.get(\"SERPAPI_API_KEY\") or None\n\n\ndef available() -> bool:\n return api_key() is not None\n\n\ndef search(params: dict) -> dict:\n \"\"\"GET serpapi.com/search.json — params SANS la clé (ajoutée ici).\"\"\"\n key = api_key()\n if not key:\n raise SerpApiError(\"SERPAPI_API_KEY absente de l'environnement\")\n q = dict(params)\n q[\"api_key\"] = key\n last = \"erreur inconnue\"\n for attempt in range(RETRIES):\n if attempt:\n time.sleep(BACKOFF * (2 ** (attempt - 1)))\n try:\n r = requests.get(BASE, params=q, timeout=TIMEOUT)\n except requests.RequestException as e:\n # ne jamais inclure l'URL complète (elle contient la clé)\n last = f\"erreur réseau: {type(e).__name__}\"\n continue\n if r.status_code == 429 or r.status_code >= 500:\n last = f\"HTTP {r.status_code}\"\n continue\n if r.status_code != 200:\n raise SerpApiError(f\"SerpApi HTTP {r.status_code}\")\n data = r.json()\n if data.get(\"error\"):\n # « fully empty » = zéro résultat, pas une panne\n if \"hasn't returned any results\" in str(data[\"error\"]):\n return data\n raise SerpApiError(f\"SerpApi: {data['error']}\")\n return data\n raise SerpApiError(f\"SerpApi injoignable après {RETRIES} tentatives ({last})\")\n\n\ndef google_maps(query: str) -> dict:\n return search({\"engine\": \"google_maps\", \"q\": query, \"hl\": \"fr\",\n \"gl\": \"ca\", \"google_domain\": \"google.ca\", \"type\": \"search\"})\n\n\ndef google_maps_reviews(data_id: str, next_page_token: str | None = None) -> dict:\n p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n p[\"next_page_token\"] = next_page_token\n return search(p)\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":785,"cache_read_input_tokens":71595,"cache_creation":{"ephemeral_5m_input_tokens":785,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cf6a7732-790c-4c68-8e1a-0a4a9bfac512"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_018pfCWgy7i1yV1rTnx1cG2t","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6c3f1fdd-6209-4792-97a3-a8bf8356a576","timestamp":"2026-08-24T22:00:03.446Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# serpapi_client.py : client SerpApi minimal, STRICTEMENT côté serveur.\n#\n# Sécurité de la clé (non négociable) :\n# - lue UNIQUEMENT depuis la variable d'environnement SERPAPI_API_KEY\n# (chargée par run.py depuis .env, jamais commitée — voir .env.example) ;\n# - jamais écrite dans les logs, les exceptions ni les réponses API ;\n# - aucune requête SerpApi ne part du navigateur : ce module n'est importé\n# que par les jobs serveur (managers.py).\n#\n# Robustesse : timeout 30 s, 3 tentatives avec repli exponentiel sur les\n# erreurs transitoires (réseau, 429, 5xx). Les erreurs définitives (4xx)\n# remontent immédiatement.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport os\nimport time\n\nimport requests\n\nBASE = \"https://serpapi.com/search.json\"\nTIMEOUT = 30\nRETRIES = 3\nBACKOFF = 2.0 # s — doublé à chaque tentative\n\n\nclass SerpApiError(RuntimeError):\n pass\n\n\ndef api_key() -> str | None:\n return os.environ.get(\"SERPAPI_API_KEY\") or None\n\n\ndef available() -> bool:\n return api_key() is not None\n\n\ndef search(params: dict) -> dict:\n \"\"\"GET serpapi.com/search.json — params SANS la clé (ajoutée ici).\"\"\"\n key = api_key()\n if not key:\n raise SerpApiError(\"SERPAPI_API_KEY absente de l'environnement\")\n q = dict(params)\n q[\"api_key\"] = key\n last = \"erreur inconnue\"\n for attempt in range(RETRIES):\n if attempt:\n time.sleep(BACKOFF * (2 ** (attempt - 1)))\n try:\n r = requests.get(BASE, params=q, timeout=TIMEOUT)\n except requests.RequestException as e:\n # ne jamais inclure l'URL complète (elle contient la clé)\n last = f\"erreur réseau: {type(e).__name__}\"\n continue\n if r.status_code == 429 or r.status_code >= 500:\n last = f\"HTTP {r.status_code}\"\n continue\n if r.status_code != 200:\n raise SerpApiError(f\"SerpApi HTTP {r.status_code}\")\n data = r.json()\n if data.get(\"error\"):\n # « fully empty » = zéro résultat, pas une panne\n if \"hasn't returned any results\" in str(data[\"error\"]):\n return data\n raise SerpApiError(f\"SerpApi: {data['error']}\")\n return data\n raise SerpApiError(f\"SerpApi injoignable après {RETRIES} tentatives ({last})\")\n\n\ndef google_maps(query: str) -> dict:\n return search({\"engine\": \"google_maps\", \"q\": query, \"hl\": \"fr\",\n \"gl\": \"ca\", \"google_domain\": \"google.ca\", \"type\": \"search\"})\n\n\ndef google_maps_reviews(data_id: str, next_page_token: str | None = None) -> dict:\n p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n p[\"next_page_token\"] = next_page_token\n return search(p)\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c579f78b-e1cb-4d7e-9248-58d529e06b42","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKomyJexy4yakRTrjRL","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1419,"cache_read_input_tokens":72380,"cache_creation":{"ephemeral_5m_input_tokens":1419,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"79e9ae7f-db86-46a5-9628-4c605d794a1d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKomyJexy4yakRTrjRL","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01D1tte4GTG3tYpV9Sd4ZjPw","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\n sources = json.load(f)[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n best, best_conf, best_sig = None, 0.0, {}\n for c in cands[:8]:\n conf, sig = _score_candidate(mgr, c)\n if conf > best_conf:\n best, best_conf, best_sig = c, conf, sig\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1419,"cache_read_input_tokens":72380,"cache_creation":{"ephemeral_5m_input_tokens":1419,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b0b7db2e-2bd7-46f7-9252-6fb17fcec9e7"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01D1tte4GTG3tYpV9Sd4ZjPw","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c62685f6-adb5-420f-987e-4d67dde521db","timestamp":"2026-08-24T22:02:05.739Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\n sources = json.load(f)[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n best, best_conf, best_sig = None, 0.0, {}\n for c in cands[:8]:\n conf, sig = _score_candidate(mgr, c)\n if conf > best_conf:\n best, best_conf, best_sig = c, conf, sig\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"ae625b2c-57aa-4192-81ce-e05d60270d82","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKxnvMocLCB6K65AHp3","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS/gIKpwEIERgCKkBcAn6VA1okMjsEid5SN7DCC9EPbR0Z6DsPT2yp3hKI//gDbM99Gy6pLiaErkXb6uy6rmlahO/EWEFGfMSixjmOMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHh/rLUBrABAhIMZwepR8WHpBs1XJ9BGgwUBFIPIbGkyCXEKSgiMLqBn3FYUkQnG9OgEhebIWxiHqC6z0tr8LcYhXJUUrwzpsAZaoQndRURnps7oxVHfCqDAT53ZdXAnArOlSXNJJ49ATQQp0h/L2xIHxpWXiCFcHaRChFkV9znosDvL4T5H6xpTM+qEApTNi+rez1pUts33h1Xv5nS7uW5pMzxdqhysnbHJ55Jj/SZlv1AgTl3wEOnWJFbNpGZnHKVIJPebIh0pDOM3rM34ndIIq0fFv6DvcxEgxQAGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11524,"cache_read_input_tokens":73799,"cache_creation":{"ephemeral_5m_input_tokens":11524,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e9804405-4688-43dd-8528-a8ab218ab454"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKxnvMocLCB6K65AHp3","type":"message","role":"assistant","content":[{"type":"text","text":"Je vérifie que `db.DATA_DIR` existe et que tout compile."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11524,"cache_read_input_tokens":73799,"cache_creation":{"ephemeral_5m_input_tokens":11524,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"48b5bbc2-cc58-4f8f-93c1-f2d3a8302888"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKxnvMocLCB6K65AHp3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01P2xiCEc4S1E81X63P1xZJH","name":"Grep","input":{"pattern":"DATA_DIR|sources\\.json","path":"/Users/simon-pierreboucher/apps/lou-ka/louka","output_mode":"content","head_limit":10},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11524,"cache_read_input_tokens":73799,"cache_creation":{"ephemeral_5m_input_tokens":11524,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"86e00d28-d036-41ec-b820-9deb8c287f2e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01P2xiCEc4S1E81X63P1xZJH","type":"tool_result","content":"louka/db.py:167: source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\nlouka/db.py:210: source_id TEXT UNIQUE, -- id de la source (data/sources.json)\nlouka/managers.py:7:# (data/sources.json, hors portails et petites annonces) qui ont au moins\nlouka/managers.py:63:# --- 1) amorçage depuis sources.json ----------------------------------------\nlouka/managers.py:71: with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\nlouka/pdfgen.py:281: reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\nlouka/pdfgen.py:971: reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\nlouka/schema.py:49: source: str # id de la source (voir data/sources.json)\nlouka/web.py:21:SOURCES_PATH = ROOT / \"data\" / \"sources.json\"\nlouka/tal.py:43:DATA_DIR = Path(__file__).resolve().parent.parent / \"data\"\n\n[Showing results with pagination = limit: 10]"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d83913f1-64f5-48a9-abf9-5be473848b0a","timestamp":"2026-08-24T22:02:11.402Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"louka/db.py:167: source_id TEXT PRIMARY KEY, -- id de la source (data/sources.json)\nlouka/db.py:210: source_id TEXT UNIQUE, -- id de la source (data/sources.json)\nlouka/managers.py:7:# (data/sources.json, hors portails et petites annonces) qui ont au moins\nlouka/managers.py:63:# --- 1) amorçage depuis sources.json ----------------------------------------\nlouka/managers.py:71: with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\nlouka/pdfgen.py:281: reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\nlouka/pdfgen.py:971: reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\nlouka/schema.py:49: source: str # id de la source (voir data/sources.json)\nlouka/web.py:21:SOURCES_PATH = ROOT / \"data\" / \"sources.json\"\nlouka/tal.py:43:DATA_DIR = Path(__file__).resolve().parent.parent / \"data\"","numLines":10,"appliedLimit":10}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKxnvMocLCB6K65AHp3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01E2KXCpgCWZeNmJcDZLyWPw","name":"Bash","input":{"command":"python3 -m py_compile louka/serpapi_client.py louka/managers.py louka/history.py louka/building.py louka/recycled.py louka/coutreel.py louka/hiver.py louka/db.py && echo OK","description":"Syntax-check new modules"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":11524,"cache_read_input_tokens":73799,"cache_creation":{"ephemeral_5m_input_tokens":11524,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9af988e0-be82-42df-86d1-4ecdd6ff9630"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01E2KXCpgCWZeNmJcDZLyWPw","type":"tool_result","content":"OK","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ba2fbcc4-52c4-4537-a875-a016f874546b","timestamp":"2026-08-24T22:02:12.492Z","tool_use_result":{"stdout":"OK","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"39c8f53e-8e19-4d27-b5d9-7caaedfe9ddc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKyHfFCYGhP7hGNsenj","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISiAMKpwEIERgCKkCIaEWNBjg7k69QSEfPViF9QTbn0cVIICzItGqtc8MLRCCJ8PC2sXnu8aZ2w2E1oHP2X6iIRrMv4kB+BGZ4p8rvMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHo/rLUBrABAhIMZKwG+PQCii+HlXI5GgwaFAfsS3sox+5Ah/0iMB7i7D8tra4FO98kAfynwj6kFJMdf6ELj7bcHWOaCM0HTgrbKaDHwWopnKBf4XJYayqNAfXcS5U/37/3QfuW6XoPZ01euCPh0eO/BW6OVOLhUzFZudPMrSRGIlby3za1qSWSQoHwvrtiPCkrMFgKopwN5i1nV+cwEKW6d8svayVJwZabPjveLcMwXtVukHAykZJs6p8d4n77ITaPdsKENxsT7FB6Ze8APOw1rHa+VK6w0SNWb2tPo7ftZTye9ASxTRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1444,"cache_read_input_tokens":85323,"cache_creation":{"ephemeral_5m_input_tokens":1444,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"45be0e06-f8d5-45d6-a6fe-49df4a6a04b8"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKyHfFCYGhP7hGNsenj","type":"message","role":"assistant","content":[{"type":"text","text":"`db.DATA_DIR` n'existe pas — le pattern du projet est `db.DB_PATH.parent`. Correction."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1444,"cache_read_input_tokens":85323,"cache_creation":{"ephemeral_5m_input_tokens":1444,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fb98cf00-fcf8-4b99-8eda-3c23c57555e0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKyHfFCYGhP7hGNsenj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JtBUaeZNsohe14PdLFeQQ9","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","old_string":" with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\n sources = json.load(f)[\"sources\"]","new_string":" sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1444,"cache_read_input_tokens":85323,"cache_creation":{"ephemeral_5m_input_tokens":1444,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5d71292b-5032-42ef-96f5-5815c4eb0f1a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JtBUaeZNsohe14PdLFeQQ9","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"842a560c-dffd-4a45-91c5-5ad463ff0679","timestamp":"2026-08-24T22:02:18.341Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","oldString":" with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\n sources = json.load(f)[\"sources\"]","newString":" sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:\n sources = json.load(f)[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n best, best_conf, best_sig = None, 0.0, {}\n for c in cands[:8]:\n conf, sig = _score_candidate(mgr, c)\n if conf > best_conf:\n best, best_conf, best_sig = c, conf, sig\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[{"oldStart":68,"oldLines":8,"newStart":68,"newLines":8,"lines":[" if own:"," con = db.connect()"," try:","- with open(db.DATA_DIR / \"sources.json\", encoding=\"utf-8\") as f:","- sources = json.load(f)[\"sources\"]","+ sources = json.loads((db.DB_PATH.parent / \"sources.json\")","+ .read_text(\"utf-8\"))[\"sources\"]"," with_ads = {r[\"source\"] for r in con.execute("," \"SELECT DISTINCT source FROM listings\")}"," n = 0"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8677b655-18be-4838-8ce8-a0a10490aadf","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKyihtExv8kse9LNLb5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01K5F5kwN2yj2CsUSsRuq96i","name":"Bash","input":{"command":"python3 -c \"\nimport sys; sys.path.insert(0, '.')\nfrom louka import managers, db\ncon = db.connect()\nprint('seed:', managers.seed(con))\nn = con.execute('SELECT COUNT(*) n FROM managers').fetchone()['n']\nprint('managers en base:', n)\nfor r in con.execute('SELECT source_id, name, website FROM managers LIMIT 5'):\n print(dict(r))\ncon.close()\n\"","description":"Test managers.seed() on prod DB"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":401,"cache_read_input_tokens":86767,"cache_creation":{"ephemeral_5m_input_tokens":401,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d228b998-24bd-44d5-86cb-a3e7c1727b65"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"Exit code 1\nTraceback (most recent call last):\n File \"\", line 3, in \n from louka import managers, db\n File \"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py\", line 31, in \n from . import db, serpapi_client\n File \"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py\", line 22, in \n import requests\nModuleNotFoundError: No module named 'requests'","is_error":true,"tool_use_id":"toolu_01K5F5kwN2yj2CsUSsRuq96i"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2ac6e70f-a4d0-4234-b13c-bdbce76e55c4","timestamp":"2026-08-24T22:02:23.317Z","tool_use_result":"Error: Exit code 1\nTraceback (most recent call last):\n File \"\", line 3, in \n from louka import managers, db\n File \"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py\", line 31, in \n from . import db, serpapi_client\n File \"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py\", line 22, in \n import requests\nModuleNotFoundError: No module named 'requests'"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"4f4c83fe-ac4b-44c0-95a2-1880623dba51","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKz5z3xvNtPwtD6V5GX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QKPcMXkaQFZqzA5HFHbZ21","name":"Bash","input":{"command":"pm2 jlist 2>/dev/null | python3 -c \"import json,sys; [print(p['name'], p['pm2_env'].get('pm_exec_path'), p['pm2_env'].get('exec_interpreter')) for p in json.load(sys.stdin)]\" ; ls /Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python 2>/dev/null; head -5 /Users/simon-pierreboucher/apps/lou-ka/run.py","description":"Find project Python interpreter (venv)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":413,"cache_read_input_tokens":87168,"cache_creation":{"ephemeral_5m_input_tokens":413,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a39393b0-4776-4ced-a99d-d98604e8e036"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QKPcMXkaQFZqzA5HFHbZ21","type":"tool_result","content":"immbot-ai /Users/simon-pierreboucher/apps/immbot-ai/node_modules/.bin/next none\nimmbot-ngrok /opt/homebrew/bin/ngrok none\nairiskindex-web /Users/simon-pierreboucher/apps/airiskindex/infra/run-web.sh bash\nairiskindex-ngrok /Users/simon-pierreboucher/apps/airiskindex/infra/run-ngrok.sh bash\nllmindex-web /Users/simon-pierreboucher/apps/llmindex/apps/web/.next/standalone/apps/web/server.js node\nllmindex-ngrok /opt/homebrew/bin/ngrok none\nllmindex-worker /Users/simon-pierreboucher/apps/llmindex/apps/worker/node_modules/tsx/dist/cli.mjs /opt/homebrew/Cellar/node/25.9.0_1/bin/node\nllmindex-benchmark /Users/simon-pierreboucher/apps/llmindex/apps/worker/node_modules/tsx/dist/cli.mjs /opt/homebrew/Cellar/node/25.9.0_1/bin/node\ncoinexplorer-indexer /Users/simon-pierreboucher/apps/coinexplorer/.venv/bin/python none\ncoinexplorer-api /Users/simon-pierreboucher/apps/coinexplorer/.venv/bin/python none\ncoinexplorer-ngrok /opt/homebrew/bin/ngrok none\nvoyage2026 /opt/homebrew/bin/node none\nvoyage2026-ngrok /opt/homebrew/bin/ngrok none\nlou-ka-web /Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python none\nlou-ka-sync /Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python none\nlou-ka-ngrok /opt/homebrew/bin/ngrok none\ndraft2027 /opt/homebrew/bin/node none\ndraft2027-ngrok /opt/homebrew/bin/ngrok none\ngroupe-ka /Users/simon-pierreboucher/apps/groupe-ka/node_modules/.bin/next none\ngroupe-ka-ngrok /opt/homebrew/bin/ngrok none\nearthnow-api /opt/homebrew/bin/node none\nearthnow-web /Users/simon-pierreboucher/apps/earth-now/apps/web/node_modules/.bin/next none\nearthnow-proxy /opt/homebrew/bin/node none\nearthnow-ngrok /opt/homebrew/bin/ngrok none\nkhaelor-sh /opt/homebrew/bin/node none\nkhaelor-sh-ngrok /opt/homebrew/bin/ngrok none\nspbdrive-server /Users/simon-pierreboucher/srv/drive/app/src/server.mjs node\nspbdrive-tunnel /opt/homebrew/bin/ngrok none\nsvgarden-web /Users/simon-pierreboucher/apps/svgarden/server.mjs node\nsvgarden-tunnel /opt/homebrew/bin/ngrok none\nlocalvm-web /Users/simon-pierreboucher/localvm-web/server.js node\nlocalvm-web-ngrok /opt/homebrew/bin/ngrok none\nmodelmap-web /Users/simon-pierreboucher/modelmap-web/server.js node\nmodelmap-web-ngrok /opt/homebrew/bin/ngrok none\nanomaly-atlas /opt/homebrew/bin/node none\nanomaly-atlas-ngrok /opt/homebrew/bin/ngrok none\napika-api /opt/api-ka/venv/bin/uvicorn none\napika-scheduler /opt/api-ka/venv/bin/python none\napika-ngrok /opt/homebrew/bin/ngrok none\nresto-ka /Users/simon-pierreboucher/apps/resto-ka/.venv/bin/python none\nresto-ka-ngrok /opt/homebrew/bin/ngrok none\nresto-ka-sync /Users/simon-pierreboucher/apps/resto-ka/.venv/bin/python none\ncrea-ka-web /Users/simon-pierreboucher/apps/crea-ka/.venv/bin/python none\ncrea-ka-ngrok /opt/homebrew/bin/ngrok none\ncrea-ka-sync /Users/simon-pierreboucher/apps/crea-ka/.venv/bin/python none\ngroupe-ka-status-tick /Users/simon-pierreboucher/apps/groupe-ka/scripts/status-tick.sh bash\ngroupe-ka-rapport-quotidien /Users/simon-pierreboucher/apps/groupe-ka/scripts/rapport-quotidien.sh bash\n/Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python\n#!/usr/bin/env python3\n# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# run.py : point d'entrée — `sync`, `watch`, `serve`","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"51f97f70-52f9-417b-b276-a309bc0e4d04","timestamp":"2026-08-24T22:02:28.162Z","tool_use_result":{"stdout":"immbot-ai /Users/simon-pierreboucher/apps/immbot-ai/node_modules/.bin/next none\nimmbot-ngrok /opt/homebrew/bin/ngrok none\nairiskindex-web /Users/simon-pierreboucher/apps/airiskindex/infra/run-web.sh bash\nairiskindex-ngrok /Users/simon-pierreboucher/apps/airiskindex/infra/run-ngrok.sh bash\nllmindex-web /Users/simon-pierreboucher/apps/llmindex/apps/web/.next/standalone/apps/web/server.js node\nllmindex-ngrok /opt/homebrew/bin/ngrok none\nllmindex-worker /Users/simon-pierreboucher/apps/llmindex/apps/worker/node_modules/tsx/dist/cli.mjs /opt/homebrew/Cellar/node/25.9.0_1/bin/node\nllmindex-benchmark /Users/simon-pierreboucher/apps/llmindex/apps/worker/node_modules/tsx/dist/cli.mjs /opt/homebrew/Cellar/node/25.9.0_1/bin/node\ncoinexplorer-indexer /Users/simon-pierreboucher/apps/coinexplorer/.venv/bin/python none\ncoinexplorer-api /Users/simon-pierreboucher/apps/coinexplorer/.venv/bin/python none\ncoinexplorer-ngrok /opt/homebrew/bin/ngrok none\nvoyage2026 /opt/homebrew/bin/node none\nvoyage2026-ngrok /opt/homebrew/bin/ngrok none\nlou-ka-web /Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python none\nlou-ka-sync /Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python none\nlou-ka-ngrok /opt/homebrew/bin/ngrok none\ndraft2027 /opt/homebrew/bin/node none\ndraft2027-ngrok /opt/homebrew/bin/ngrok none\ngroupe-ka /Users/simon-pierreboucher/apps/groupe-ka/node_modules/.bin/next none\ngroupe-ka-ngrok /opt/homebrew/bin/ngrok none\nearthnow-api /opt/homebrew/bin/node none\nearthnow-web /Users/simon-pierreboucher/apps/earth-now/apps/web/node_modules/.bin/next none\nearthnow-proxy /opt/homebrew/bin/node none\nearthnow-ngrok /opt/homebrew/bin/ngrok none\nkhaelor-sh /opt/homebrew/bin/node none\nkhaelor-sh-ngrok /opt/homebrew/bin/ngrok none\nspbdrive-server /Users/simon-pierreboucher/srv/drive/app/src/server.mjs node\nspbdrive-tunnel /opt/homebrew/bin/ngrok none\nsvgarden-web /Users/simon-pierreboucher/apps/svgarden/server.mjs node\nsvgarden-tunnel /opt/homebrew/bin/ngrok none\nlocalvm-web /Users/simon-pierreboucher/localvm-web/server.js node\nlocalvm-web-ngrok /opt/homebrew/bin/ngrok none\nmodelmap-web /Users/simon-pierreboucher/modelmap-web/server.js node\nmodelmap-web-ngrok /opt/homebrew/bin/ngrok none\nanomaly-atlas /opt/homebrew/bin/node none\nanomaly-atlas-ngrok /opt/homebrew/bin/ngrok none\napika-api /opt/api-ka/venv/bin/uvicorn none\napika-scheduler /opt/api-ka/venv/bin/python none\napika-ngrok /opt/homebrew/bin/ngrok none\nresto-ka /Users/simon-pierreboucher/apps/resto-ka/.venv/bin/python none\nresto-ka-ngrok /opt/homebrew/bin/ngrok none\nresto-ka-sync /Users/simon-pierreboucher/apps/resto-ka/.venv/bin/python none\ncrea-ka-web /Users/simon-pierreboucher/apps/crea-ka/.venv/bin/python none\ncrea-ka-ngrok /opt/homebrew/bin/ngrok none\ncrea-ka-sync /Users/simon-pierreboucher/apps/crea-ka/.venv/bin/python none\ngroupe-ka-status-tick /Users/simon-pierreboucher/apps/groupe-ka/scripts/status-tick.sh bash\ngroupe-ka-rapport-quotidien /Users/simon-pierreboucher/apps/groupe-ka/scripts/rapport-quotidien.sh bash\n/Users/simon-pierreboucher/apps/lou-ka/.venv/bin/python\n#!/usr/bin/env python3\n# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# run.py : point d'entrée — `sync`, `watch`, `serve`","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"57db72d2-f725-4884-85f1-8afc5a357bda","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKzSxs6KQH9WNDxK7Q6","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01S8mjY67q59U15uUSDgg5Sf","name":"Bash","input":{"command":".venv/bin/python -c \"\nfrom louka import managers, db\ncon = db.connect()\nprint('seed:', managers.seed(con))\nn = con.execute('SELECT COUNT(*) n FROM managers').fetchone()['n']\nprint('managers en base:', n)\nfor r in con.execute('SELECT source_id, name, website FROM managers LIMIT 5'):\n print(dict(r))\ncon.close()\n\"","description":"Test managers.seed() with project venv"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2055,"cache_read_input_tokens":87581,"cache_creation":{"ephemeral_5m_input_tokens":2055,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"af9e9914-379b-4456-9e0e-2f847abcf53a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01S8mjY67q59U15uUSDgg5Sf","type":"tool_result","content":"seed: {'managers': 203}\nmanagers en base: 203\n{'source_id': 'lafrance_mathieu', 'name': 'Lafrance & Mathieu', 'website': 'https://lafrance-mathieu.com'}\n{'source_id': 'msi', 'name': 'MSI Gestion immobilière', 'website': 'https://www.msimmobiliers.com'}\n{'source_id': 'logisco', 'name': 'Logisco', 'website': 'https://logisco.com/fr'}\n{'source_id': 'laberge', 'name': 'Groupe Laberge', 'website': 'https://www.laberge.qc.ca'}\n{'source_id': 'cogir', 'name': 'Cogir Immobilier', 'website': 'https://www.cogir.net'}","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4dff5e5e-def3-4e28-9670-ddf8f9c6a283","timestamp":"2026-08-24T22:02:33.061Z","tool_use_result":{"stdout":"seed: {'managers': 203}\nmanagers en base: 203\n{'source_id': 'lafrance_mathieu', 'name': 'Lafrance & Mathieu', 'website': 'https://lafrance-mathieu.com'}\n{'source_id': 'msi', 'name': 'MSI Gestion immobilière', 'website': 'https://www.msimmobiliers.com'}\n{'source_id': 'logisco', 'name': 'Logisco', 'website': 'https://logisco.com/fr'}\n{'source_id': 'laberge', 'name': 'Groupe Laberge', 'website': 'https://www.laberge.qc.ca'}\n{'source_id': 'cogir', 'name': 'Cogir Immobilier', 'website': 'https://www.cogir.net'}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"1a906146-0318-46ff-906b-82bf8b0f8a5b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKzoZrub3TRUfSFJj3n","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISmQQKpwEIERgCKkD92Y3wI75jbj8aoXZa+wzaJSFsXyni+3ScI0rZkRaQkbD5+ij/oFo3Enu20dodaqhuyFD/WeyfQE0Z8QE345IZMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH+/rLUBrABAhIMrcYEJ6wdmsbdVDNaGgyNT5YR86eFEgM5Ss4iMIngdrl5tcZhwFLjth1WA5Unl56tfwGTo655YL8jXKdokCRkqLtr9zYgp2+yaNtxCSqeAl+fz2EKmYyv3GHlLVOt/ei01tsGp8HrkIpJzLf+1RR2v2PwzpKM6YfB+3dD9Xia30ln3rtkQtDEcyHe2dMAOcfPLNjuRUkedKIRzrdwu5NLSv6M2huMoTqRUG51KwMUgEtWgtoHIRapYjLojCzr+HwCFnwEnuDXY7M9C+JnwpUIRXDwZp9HKXP9FPFnSg9jABHYE7ICzIPQCzM0bkeygsUHQ1MCUZkw3pPcVEREPgo+dHUp0ltAG+K04xc0QkvVJD/0SblylHue74WTGz55jqM5QuuVo0l+r6T7Gl2pX3qSTjmovEswI/62fXURf6ZBzpf3+tHGtwlnhE8kMhjryIv4FWn0bYYC9AQkm5oyi3i7G3W+EwBTDQ7WT42ZciQYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":486,"cache_read_input_tokens":89636,"cache_creation":{"ephemeral_5m_input_tokens":486,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1952328d-063f-4729-a99f-30c01dec7534"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKzoZrub3TRUfSFJj3n","type":"message","role":"assistant","content":[{"type":"text","text":"Amorçage : 203 gestionnaires. Test live de la résolution Google Maps + synchro des avis sur un cas réel (Logisco)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":486,"cache_read_input_tokens":89636,"cache_creation":{"ephemeral_5m_input_tokens":486,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"07c7ddf6-7ae0-4a09-bf3f-57855945ccc4"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNKzoZrub3TRUfSFJj3n","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HSbpSGQeWRroie9XtNwfa3","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import managers, db\ncon = db.connect()\nmid = con.execute(\\\"SELECT id FROM managers WHERE source_id='logisco'\\\").fetchone()['id']\nprint('resolve:', managers.resolve(mid, con))\nr = dict(con.execute('SELECT gmaps_name, gmaps_address, gmaps_rating, gmaps_reviews, match_confidence, match_signals FROM managers WHERE id=?', (mid,)).fetchone())\nprint(r)\nprint('sync:', managers.sync_reviews(mid, con, max_pages=3))\nimport json\nstats = json.loads(con.execute('SELECT review_stats FROM managers WHERE id=?', (mid,)).fetchone()['review_stats'])\nprint(json.dumps(stats, ensure_ascii=False, indent=1)[:1200])\ncon.close()\n\" 2>&1 | grep -v cede959","description":"Live test resolve + sync_reviews (Logisco)","timeout":180000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":486,"cache_read_input_tokens":89636,"cache_creation":{"ephemeral_5m_input_tokens":486,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"edb3779e-3fc7-4f8a-b867-7669dd28e60d"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bej6dyp4n","tool_use_id":"toolu_01HSbpSGQeWRroie9XtNwfa3","description":"Live test resolve + sync_reviews (Logisco)","task_type":"local_bash","uuid":"4054aa31-959e-4841-8690-f81bec9198df","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HSbpSGQeWRroie9XtNwfa3","type":"tool_result","content":"resolve: {'ok': True, 'gmaps_name': 'Logisco', 'confiance': 1.0}\n{'gmaps_name': 'Logisco', 'gmaps_address': '11615 Bd de la Colline, Québec, QC G2A 2E1', 'gmaps_rating': 1.0, 'gmaps_reviews': 1, 'match_confidence': 1.0, 'match_signals': '{\"similarite_nom\": 1.0, \"meme_domaine_web\": \"logisco.com\", \"ville_correspondante\": \"Québec\"}'}\nsync: {'ok': True, 'nouveaux': 1, 'pages': 1, 'total': 1}\n{\n \"n\": 1,\n \"version\": \"gestionnaire-1.0\",\n \"statut\": \"calculated\",\n \"methode\": \"statistiques calculées sur les avis Google synchronisés en base (pas seulement la moyenne affichée par Google) ; thèmes par lexique — inférence, pas une lecture humaine\",\n \"moyenne\": 1.0,\n \"distribution\": {\n \"1\": 1,\n \"2\": 0,\n \"3\": 0,\n \"4\": 0,\n \"5\": 0\n },\n \"pct_negatif\": 100,\n \"pct_positif\": 0,\n \"avec_reponse_proprietaire\": 0,\n \"themes\": {\n \"communication\": {\n \"mentions\": 1,\n \"negatif\": 1,\n \"positif\": 0\n },\n \"service client\": {\n \"mentions\": 1,\n \"negatif\": 1,\n \"positif\": 0\n },\n \"stationnement\": {\n \"mentions\": 1,\n \"negatif\": 1,\n \"positif\": 0\n }\n },\n \"plaintes_frequentes\": []\n}","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1b276ec5-705b-466d-be17-300a1087f058","timestamp":"2026-08-24T22:02:48.664Z","tool_use_result":{"stdout":"resolve: {'ok': True, 'gmaps_name': 'Logisco', 'confiance': 1.0}\n{'gmaps_name': 'Logisco', 'gmaps_address': '11615 Bd de la Colline, Québec, QC G2A 2E1', 'gmaps_rating': 1.0, 'gmaps_reviews': 1, 'match_confidence': 1.0, 'match_signals': '{\"similarite_nom\": 1.0, \"meme_domaine_web\": \"logisco.com\", \"ville_correspondante\": \"Québec\"}'}\nsync: {'ok': True, 'nouveaux': 1, 'pages': 1, 'total': 1}\n{\n \"n\": 1,\n \"version\": \"gestionnaire-1.0\",\n \"statut\": \"calculated\",\n \"methode\": \"statistiques calculées sur les avis Google synchronisés en base (pas seulement la moyenne affichée par Google) ; thèmes par lexique — inférence, pas une lecture humaine\",\n \"moyenne\": 1.0,\n \"distribution\": {\n \"1\": 1,\n \"2\": 0,\n \"3\": 0,\n \"4\": 0,\n \"5\": 0\n },\n \"pct_negatif\": 100,\n \"pct_positif\": 0,\n \"avec_reponse_proprietaire\": 0,\n \"themes\": {\n \"communication\": {\n \"mentions\": 1,\n \"negatif\": 1,\n \"positif\": 0\n },\n \"service client\": {\n \"mentions\": 1,\n \"negatif\": 1,\n \"positif\": 0\n },\n \"stationnement\": {\n \"mentions\": 1,\n \"negatif\": 1,\n \"positif\": 0\n }\n },\n \"plaintes_frequentes\": []\n}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"90ade6bb-3772-483c-88a0-6c310234b74e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bej6dyp4n","tool_use_id":"toolu_01HSbpSGQeWRroie9XtNwfa3","status":"completed","output_file":"","summary":"Live test resolve + sync_reviews (Logisco)","uuid":"51210f79-4335-4fb7-be9a-f7b6944f5a38","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL1xTgEjCWY4gwEu7g5","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISoAYKpwEIERgCKkDqi79cLueMcVTgc2u+frgN5YwU/cvwHnS1ZkhDZXCoyxGk6zSE5kXKKIRVyr34kAZpnNtHKBuPBYrOj1NGUbPqMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGR/7LUBrABAhIMsvHdrhS4tcPUyNVOGgzUIVYvc+717rYeSK8iMEMYM1BVpbrc5RRXEVeLBmG0LLwmUpp+xax7yRZlGdOxC0jEtHf0R39M2kTi+5N4xiqlBH0V1SPdlvDHW5WautCQOQ+HOMUYFJFcbXSbtDBmvesMbaSqUSewjKTfb4EMtaPGumMfteU2WrErzDbJ53oxR5XYZrL7Jgi4KwFBLPfKX4AzNcMpnC8tTYU+/A/NjoM6xgUR/AbYwOiTm7McQM5h3xEUBdOgpOK59xfKsiahcfmrWa8y7w9inEgEGv91FEaik1IYeEP9+TNEqhLOsSHJHMjOncdrh5iK3uKs+vR0atygJ+Ns9sBrEBAoJ0iwDwt45U5Jd0+4zWiXdwViYNuqdUVKXaxvI3xqNVq+3aqiWVukqNP5bzuqr4J+ohHYqTCvEjnnh8Xx2iwH2yOCB9qPGbf4pCDWTXlxRlul0wqW9JdEGemzJfsOiYwVM0OMmkulDn3YCJ9uqAQg88etYIyTTenBY0WEbATOCVsRsaKDK0aT/r4yrh1WmgqKpNqOzY80v//eDujwdvc4HCvXiDu7lAPc+5097UbU9NGx23atkyBRQFAQWjkeV/RSD9YX3U05dAO60cc6m8SheE6P9GhNyjCG2NrzltJ4/hf5Rff0QNTvFJ9mK78zErJiOXlv6Rdb1fQ4ihcDdqaRrfClUt6pZWHYEHY5lLNcW816iwKhMmkUukJq5M8voN2YImrMVIOzJdSlx6MSOlsB6Mw6RsG5xzYIlcYYx/YO5UkLBKr1d298sl5VlJdAP7lGnUdqWlssfLK4lg84pGfCO98364fD10ZL9qVuEhgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1171,"cache_read_input_tokens":90122,"cache_creation":{"ephemeral_5m_input_tokens":1171,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a2ea29b8-5333-4208-88b2-f376ef676e9b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL1xTgEjCWY4gwEu7g5","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS0wQKqAEIERgCKkBOP0pHwTZ+uaVcY86p5JcioE2QhBSHdFiIvqP16AD4z0HEoWRUL4QHdUgSRA9IpuAELYqljcGxfe7WJgPCdpGTMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagBkf+y1AawAQISDGCXujEHicJ1C9FikxoMbDeIqJc7Ur1BSw/6IjD3rkuytWsNmdXEyE0Xxn6GC0groXFSVycutd0MBToYiUD9yX+vCnh1sKzo0u9wJhwq1wIi3voYHA8/3hrh18ibKTAeDLt4YIE/VcGrEnHw5++7B3vLKAxKe62jnhOhhbJ3VaC+FAUSnR4r6ryrLNVsyfXCUsRHHe5gGe3GV/vFLjFtSTe8I+jFhC0Uc44CL3Hg9mbtlHa57geqmE99MdVRHDQFFXuZihiNpc5npfvqkVyKwKkMZmmb20GN+MkQ4KSCbeVLeKm9YLkYqHnaxglEeEQ3M4jS5gWdhf6HuXSQ5oDYcwHgOU3foNoO5u05FXaZHTi5DmLcK9U67cenAe/JWpzmkd1itPzEfOeSeTzKqbQuwo0MNaWKNUTWLScTEMIGToMau8Umh0IYCl/11ha8j2mAFhBzM96m5tCW35kTQbNyQ7lEHxYuBbAm9Xpy7RH+6QNXP2S1gXOQetxLvkbFtuoZaSuwkDZPKQqKh9Qo2kzTocqSXuJ2/U/o/YekiAWl3c++WqiODB+RGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1171,"cache_read_input_tokens":90122,"cache_creation":{"ephemeral_5m_input_tokens":1171,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ef5afca0-3fa2-4136-87c0-27bd1ab4bc5f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL1xTgEjCWY4gwEu7g5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019teSkupcLk8e2b1AdzPWvk","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","old_string":" best, best_conf, best_sig = None, 0.0, {}\n for c in cands[:8]:\n conf, sig = _score_candidate(mgr, c)\n if conf > best_conf:\n best, best_conf, best_sig = c, conf, sig","new_string":" scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1171,"cache_read_input_tokens":90122,"cache_creation":{"ephemeral_5m_input_tokens":1171,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fa55d33e-29ec-4c5b-9a10-3f4ab42b84bb"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019teSkupcLk8e2b1AdzPWvk","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4a053eb3-25fc-494f-9e11-b22498d9d858","timestamp":"2026-08-24T22:03:00.832Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","oldString":" best, best_conf, best_sig = None, 0.0, {}\n for c in cands[:8]:\n conf, sig = _score_candidate(mgr, c)\n if conf > best_conf:\n best, best_conf, best_sig = c, conf, sig","newString":" scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n best, best_conf, best_sig = None, 0.0, {}\n for c in cands[:8]:\n conf, sig = _score_candidate(mgr, c)\n if conf > best_conf:\n best, best_conf, best_sig = c, conf, sig\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[{"oldStart":143,"oldLines":11,"newStart":143,"newLines":15,"lines":[" if not cands and data.get(\"place_results\"):"," cands = [data[\"place_results\"]]"," ","+ scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]"," best, best_conf, best_sig = None, 0.0, {}","- for c in cands[:8]:","- conf, sig = _score_candidate(mgr, c)","- if conf > best_conf:","- best, best_conf, best_sig = c, conf, sig","+ if scored:","+ top = max(s[0] for s in scored)","+ # plusieurs fiches quasi équivalentes (chaîne avec une fiche par","+ # immeuble) : préférer la plus représentative (le plus d'avis)","+ near = [s for s in scored if s[0] >= top - 0.05]","+ best_conf, best_sig, best = max(","+ near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))"," now = time.time()"," if best is None or best_conf < SEUIL_CONFIANCE:"," con.execute("]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"662e1f8d-a2f2-4ef7-bffc-af0d084d2195","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL2sAThv5Fu1SHPu7zu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Xh4xzEQXaAT9WSJQSAPVpg","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import managers, db\ncon = db.connect()\nmid = con.execute(\\\"SELECT id FROM managers WHERE source_id='logisco'\\\").fetchone()['id']\ncon.execute('DELETE FROM manager_reviews WHERE manager_id=?', (mid,))\ncon.execute('UPDATE managers SET resolved_at=NULL, review_stats=NULL, last_synced_at=NULL WHERE id=?', (mid,))\ncon.commit()\nprint('resolve:', managers.resolve(mid, con))\nr = dict(con.execute('SELECT gmaps_name, gmaps_address, gmaps_rating, gmaps_reviews, match_confidence FROM managers WHERE id=?', (mid,)).fetchone())\nprint(r)\nprint('sync:', managers.sync_reviews(mid, con, max_pages=3))\nimport json\nstats = json.loads(con.execute('SELECT review_stats FROM managers WHERE id=?', (mid,)).fetchone()['review_stats'])\nprint({k: stats.get(k) for k in ('n','moyenne','distribution','pct_negatif','pct_positif','moyenne_12m','n_12m','tendance','plaintes_frequentes')})\n# re-sync incrémental : doit s'arrêter vite, 0 nouveau\nprint('re-sync:', managers.sync_reviews(mid, con, max_pages=3))\ncon.close()\n\" 2>&1 | grep -v cede959","description":"Re-test resolve with tie-break + incremental sync","timeout":180000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":797,"cache_read_input_tokens":91293,"cache_creation":{"ephemeral_5m_input_tokens":797,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c935a364-f4e4-4c1c-bfb9-cbdefe0931aa"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Xh4xzEQXaAT9WSJQSAPVpg","type":"tool_result","content":"resolve: {'ok': True, 'gmaps_name': 'Logisco', 'confiance': 1.0}\n{'gmaps_name': 'Logisco', 'gmaps_address': '11615 Bd de la Colline, Québec, QC G2A 2E1', 'gmaps_rating': 1.0, 'gmaps_reviews': 1, 'match_confidence': 1.0}\nsync: {'ok': True, 'nouveaux': 1, 'pages': 1, 'total': 1}\n{'n': 1, 'moyenne': 1.0, 'distribution': {'1': 1, '2': 0, '3': 0, '4': 0, '5': 0}, 'pct_negatif': 100, 'pct_positif': 0, 'moyenne_12m': None, 'n_12m': None, 'tendance': None, 'plaintes_frequentes': []}\nre-sync: {'ok': True, 'nouveaux': 0, 'pages': 1, 'total': 1}","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3874e314-93aa-4164-9999-e3aad3299c84","timestamp":"2026-08-24T22:03:09.297Z","tool_use_result":{"stdout":"resolve: {'ok': True, 'gmaps_name': 'Logisco', 'confiance': 1.0}\n{'gmaps_name': 'Logisco', 'gmaps_address': '11615 Bd de la Colline, Québec, QC G2A 2E1', 'gmaps_rating': 1.0, 'gmaps_reviews': 1, 'match_confidence': 1.0}\nsync: {'ok': True, 'nouveaux': 1, 'pages': 1, 'total': 1}\n{'n': 1, 'moyenne': 1.0, 'distribution': {'1': 1, '2': 0, '3': 0, '4': 0, '5': 0}, 'pct_negatif': 100, 'pct_positif': 0, 'moyenne_12m': None, 'n_12m': None, 'tendance': None, 'plaintes_frequentes': []}\nre-sync: {'ok': True, 'nouveaux': 0, 'pages': 1, 'total': 1}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"1c859b2f-4e63-4c04-8a38-fb35ae0e12bb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL3UbvZTCWCiwU3p4WX","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS6wMKpwEIERgCKkC8VapfkpdsujiAL9czeQbeyjsmLvjdNZF0Sb4gaeBezxxbe7DBPo/2euSdvU4IKiu09rUkl2ejeYiXN6UeN6ppMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGi/7LUBrABAhIMWhbLl/+j2pzKOUIpGgxtxCSxndUH8HWw6mMiMKa7OFItfGLrLS9kfUqemyWRlxE8wtzikUB9cOkPSpkepUkJjSIqMWwPw0OYzxPBJCrwASWkjpdwNozBNlmwlJkqcZPvYv5dudlzWh+5ACBTDU8D4rv6uSN6JkxxAXRGrqkBhtMSmyNyScwnbK7Bzh86WIC/R2tzsx6vyMIZ4mDn1bMbuGl2e+I4CB+QN0zEPp4Wec3k62lwHQkDnZq9KBrxS9T00E2DJxbKymNpDFHxjlj5SfiDVVrn/DAji5wTwV2bLUR39OyWvJvmL1qIi3uu+AB5nzd9HYo+vsPcyQkCTnqseyZeOw5eTpIumuKq4gJElg0+pdZJm3vQwhOWpnsm79cbB9PVOkYb9jUk96qiYQsppxwlH/ITmM8aF/YtOdhUbBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":962,"cache_read_input_tokens":92090,"cache_creation":{"ephemeral_5m_input_tokens":962,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2eeac582-4294-401b-b505-3f48031960ad"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL3UbvZTCWCiwU3p4WX","type":"message","role":"assistant","content":[{"type":"text","text":"Toujours la succursale à 1 avis — j'inspecte les candidats bruts de SerpApi pour comprendre."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":962,"cache_read_input_tokens":92090,"cache_creation":{"ephemeral_5m_input_tokens":962,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"89cef5f6-3123-4548-956b-5f9fe4cd5863"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL3UbvZTCWCiwU3p4WX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_018kB9PzAZjcDRPaH4NjdAeU","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import serpapi_client\nd = serpapi_client.google_maps('Logisco Québec Québec')\nlr = d.get('local_results') or ([d['place_results']] if d.get('place_results') else [])\nfor c in lr[:8]:\n print(repr(c.get('title')), '| reviews:', c.get('reviews'), '| rating:', c.get('rating'), '| addr:', c.get('address'), '| website:', c.get('website'), '| type:', c.get('type'))\n\" 2>&1 | grep -v cede959","description":"Inspect raw SerpApi candidates for Logisco","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":962,"cache_read_input_tokens":92090,"cache_creation":{"ephemeral_5m_input_tokens":962,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7253dc07-884f-4213-8f7b-cbb7881c675f"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_018kB9PzAZjcDRPaH4NjdAeU","type":"tool_result","content":"'Logisco - Siège social' | reviews: 395 | rating: 3.6 | addr: 950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7 | website: https://logisco.com/fr | type: Agence immobilière\n'Latté - LOGISCO' | reviews: 20 | rating: 3.5 | addr: 700 Rue d'Everell, Québec, QC G1C 0P3 | website: https://logisco.com/fr/appartements-a-louer/quebec/beauport/le-latte | type: Édifice à appartements\n'Le Prisme - Logisco, Phase 5' | reviews: 23 | rating: 3 | addr: 2905 Rue du Grand Voyer, Québec, QC G1W 3Z8 | website: https://logisco.com/fr/appartements-a-louer/quebec/sainte-foy-sillery-cap-rouge/le-prisme | type: Édifice à appartements\n'Îlot Francis-Byrne - Logisco' | reviews: 1 | rating: 5 | addr: 605 52e Rue O, Québec, QC G1H 6Y1 | website: https://logisco.com/fr/appartements-a-louer/quebec/charlesbourg/ilot-francis-byrne | type: Édifice à appartements\n'Logisco' | reviews: 1 | rating: 1 | addr: 11615 Bd de la Colline, Québec, QC G2A 2E1 | website: https://logisco.com/ | type: Gîte\n'Le Boréal - Logisco' | reviews: 5 | rating: 3.4 | addr: 1137 Rte de l'Église, Québec City, Quebec G1V 3W4 | website: https://logisco.com/appartements-a-louer/sainte-foy/653 | type: Édifice à appartements\n'Appartement logisco (795 Boulevard Wilfrid-Hamel)' | reviews: None | rating: None | addr: 795 Bd Wilfrid-Hamel, Québec, QC G1M 2R1 | website: None | type: Édifice à appartements\n'Place Paul-Gury - Logisco' | reviews: 2 | rating: 3 | addr: 6100 Rue Paul Gury, Québec, QC G1P 4R3 | website: https://logisco.com/fr/appartements-a-louer/quebec/duberger-les-saules/place-paul-gury | type: Édifice à appartements","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"79c6cdc3-48b1-4378-b59c-c2eda9a30bbc","timestamp":"2026-08-24T22:03:17.058Z","tool_use_result":{"stdout":"'Logisco - Siège social' | reviews: 395 | rating: 3.6 | addr: 950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7 | website: https://logisco.com/fr | type: Agence immobilière\n'Latté - LOGISCO' | reviews: 20 | rating: 3.5 | addr: 700 Rue d'Everell, Québec, QC G1C 0P3 | website: https://logisco.com/fr/appartements-a-louer/quebec/beauport/le-latte | type: Édifice à appartements\n'Le Prisme - Logisco, Phase 5' | reviews: 23 | rating: 3 | addr: 2905 Rue du Grand Voyer, Québec, QC G1W 3Z8 | website: https://logisco.com/fr/appartements-a-louer/quebec/sainte-foy-sillery-cap-rouge/le-prisme | type: Édifice à appartements\n'Îlot Francis-Byrne - Logisco' | reviews: 1 | rating: 5 | addr: 605 52e Rue O, Québec, QC G1H 6Y1 | website: https://logisco.com/fr/appartements-a-louer/quebec/charlesbourg/ilot-francis-byrne | type: Édifice à appartements\n'Logisco' | reviews: 1 | rating: 1 | addr: 11615 Bd de la Colline, Québec, QC G2A 2E1 | website: https://logisco.com/ | type: Gîte\n'Le Boréal - Logisco' | reviews: 5 | rating: 3.4 | addr: 1137 Rte de l'Église, Québec City, Quebec G1V 3W4 | website: https://logisco.com/appartements-a-louer/sainte-foy/653 | type: Édifice à appartements\n'Appartement logisco (795 Boulevard Wilfrid-Hamel)' | reviews: None | rating: None | addr: 795 Bd Wilfrid-Hamel, Québec, QC G1M 2R1 | website: None | type: Édifice à appartements\n'Place Paul-Gury - Logisco' | reviews: 2 | rating: 3 | addr: 6100 Rue Paul Gury, Québec, QC G1P 4R3 | website: https://logisco.com/fr/appartements-a-louer/quebec/duberger-les-saules/place-paul-gury | type: Édifice à appartements","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c854675a-07a9-4d39-b5ce-64f5bf2a7451","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL43qw1kRAHWyr3uwhD","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1823,"cache_read_input_tokens":93052,"cache_creation":{"ephemeral_5m_input_tokens":1823,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a7618c70-2eba-42d4-8b82-c6a027513eef"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL43qw1kRAHWyr3uwhD","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISxAQKqAEIERgCKkAO6t38DOGo+SeCTMmH2XHPVtDOlyaB+HWgs7j37Acq7ii95ti86Xlkrn7vfOYjaTmKgHMBjWOBZ9UBelln4e0kMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagBuf+y1AawAQISDJd4u12r2DJqZal+7RoMtcJkz4n312TrA41OIjDdbxn+TjOMVYVNqmXoVtFxkegcVp97M6ySgD34ukuQjhZP27oX5PwwUYBoc9W7KBoqyAJiB+ZyNwh3erG52I1zETjttC02Hl9XapsOesEzsN/zutR8JnAAMQTf/NOfKGdUrdCa0KAnRp6gvQ5rooR1+Nq0svh3SpNeLZFnpU5PAw6KFmzAd6DT4bXmQTrPnsSvk9zA/zV/FBs5455kNxmZc2NIBpUsbu5/8pUTokdpQXCVomtWf6Wm7bj5u65xw+PGgaYzewCj5/fEI87/Nj7aMLiYuQI1WgjnXyq3TGxyjb9s+x5H5pZliqvqMjyynxoBlJsmKBqtf1WymAmpR16ioVO4WQZPpBk2uh2APixlAW3T/KZIUMuC8VSkGoRWAHN/VeFCiW8V4SX5mef9wDvIJ/PuBRHU0Gg+h5zdmL+q3++uOmdfpe9JBIEKGAz8Aa4HFAg/jqmJyBnM1jq8pye+NvixAw7Ut3xLaps9oZmYecAobr8YQS0lMLTQGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1823,"cache_read_input_tokens":93052,"cache_creation":{"ephemeral_5m_input_tokens":1823,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3ba0f6e4-f364-46ab-bf5c-1f058032268c"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL43qw1kRAHWyr3uwhD","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XvcbzkhGVJMEW79qhoBNvf","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","old_string":" r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")","new_string":" r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|siege|social|bureau|head|office|\"\n r\"les|le|la|de|du|des|et|and)\\b\")\n\n# types Google d'une fiche d'ENTREPRISE (vs fiche d'un immeuble précis)\n_CORP_TYPES = re.compile(\n r\"agence immobili|gestion immobili|property management|real estate|\"\n r\"societe de gestion|immobilier\", re.I)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1823,"cache_read_input_tokens":93052,"cache_creation":{"ephemeral_5m_input_tokens":1823,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"49e218c9-1e1c-4664-a2ec-e8a20d0d73ac"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XvcbzkhGVJMEW79qhoBNvf","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a0efec25-da4a-4ec9-943d-2fd7a13b5966","timestamp":"2026-08-24T22:03:41.697Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","oldString":" r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")","newString":" r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|siege|social|bureau|head|office|\"\n r\"les|le|la|de|du|des|et|and)\\b\")\n\n# types Google d'une fiche d'ENTREPRISE (vs fiche d'un immeuble précis)\n_CORP_TYPES = re.compile(\n r\"agence immobili|gestion immobili|property management|real estate|\"\n r\"societe de gestion|immobilier\", re.I)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[{"oldStart":42,"oldLines":9,"newStart":42,"newLines":15,"lines":[" r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\""," r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\""," r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"","- r\"résidences?|habitations?|les|le|la|de|du|des|et|and)\\b\")","+ r\"résidences?|habitations?|siege|social|bureau|head|office|\"","+ r\"les|le|la|de|du|des|et|and)\\b\")"," ","+# types Google d'une fiche d'ENTREPRISE (vs fiche d'un immeuble précis)","+_CORP_TYPES = re.compile(","+ r\"agence immobili|gestion immobili|property management|real estate|\"","+ r\"societe de gestion|immobilier\", re.I)"," ","+"," def _norm_name(s: str | None) -> str:"," s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())"," s = \"\".join(c for c in s if not unicodedata.combining(c))"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f65ec808-db34-4683-a806-dce616095f55","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL5sqEH3C6jzSVpy6ht","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BZKsYy6A9Kst7Xmw2MVBux","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","old_string":" addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals","new_string":" addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n if _CORP_TYPES.search(cand.get(\"type\") or \"\"):\n signals[\"fiche_entreprise\"] = cand.get(\"type\")\n signals[\"type_fiche\"] = cand.get(\"type\")\n signals[\"nombre_avis\"] = cand.get(\"reviews\")\n return min(1.0, conf), signals"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1645,"cache_read_input_tokens":94875,"cache_creation":{"ephemeral_5m_input_tokens":1645,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4a3b529a-9439-46fd-ba42-1ed86dfca272"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BZKsYy6A9Kst7Xmw2MVBux","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7a44d46c-2b5d-4e24-a08b-b0d0f0b574ed","timestamp":"2026-08-24T22:03:47.988Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","oldString":" addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals","newString":" addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n if _CORP_TYPES.search(cand.get(\"type\") or \"\"):\n signals[\"fiche_entreprise\"] = cand.get(\"type\")\n signals[\"type_fiche\"] = cand.get(\"type\")\n signals[\"nombre_avis\"] = cand.get(\"reviews\")\n return min(1.0, conf), signals","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|siege|social|bureau|head|office|\"\n r\"les|le|la|de|du|des|et|and)\\b\")\n\n# types Google d'une fiche d'ENTREPRISE (vs fiche d'un immeuble précis)\n_CORP_TYPES = re.compile(\n r\"agence immobili|gestion immobili|property management|real estate|\"\n r\"societe de gestion|immobilier\", re.I)\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[{"oldStart":123,"oldLines":6,"newStart":123,"newLines":10,"lines":[" if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):"," conf += 0.1"," signals[\"ville_correspondante\"] = region","+ if _CORP_TYPES.search(cand.get(\"type\") or \"\"):","+ signals[\"fiche_entreprise\"] = cand.get(\"type\")","+ signals[\"type_fiche\"] = cand.get(\"type\")","+ signals[\"nombre_avis\"] = cand.get(\"reviews\")"," return min(1.0, conf), signals"," "," "]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c76f5177-63fd-4429-9781-15e5610d719b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL6Ktt3X6WcZYYtcEL2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0127mjEskgdpeZkJ6SFUdztU","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","old_string":" scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))","new_string":" scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # une chaîne a souvent une fiche Google PAR IMMEUBLE en plus du\n # siège : parmi les candidats plausibles (confiance proche du\n # meilleur ET au-dessus du seuil), préférer la fiche d'entreprise\n # puis la plus représentative (le plus d'avis)\n near = [s for s in scored\n if s[0] >= max(SEUIL_CONFIANCE, top - 0.15)]\n if near:\n best_conf, best_sig, best = max(\n near, key=lambda s: (bool(_CORP_TYPES.search(\n s[2].get(\"type\") or \"\")),\n s[2].get(\"reviews\") or 0, s[0]))\n else:\n best_conf, best_sig, best = max(scored, key=lambda s: s[0])"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":461,"cache_read_input_tokens":96520,"cache_creation":{"ephemeral_5m_input_tokens":461,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8947e9fd-77d8-4c60-82e1-05fa5b06d35a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0127mjEskgdpeZkJ6SFUdztU","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9addf5f7-d1dd-4796-8c00-21a745a7d8a4","timestamp":"2026-08-24T22:03:57.180Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","oldString":" scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))","newString":" scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # une chaîne a souvent une fiche Google PAR IMMEUBLE en plus du\n # siège : parmi les candidats plausibles (confiance proche du\n # meilleur ET au-dessus du seuil), préférer la fiche d'entreprise\n # puis la plus représentative (le plus d'avis)\n near = [s for s in scored\n if s[0] >= max(SEUIL_CONFIANCE, top - 0.15)]\n if near:\n best_conf, best_sig, best = max(\n near, key=lambda s: (bool(_CORP_TYPES.search(\n s[2].get(\"type\") or \"\")),\n s[2].get(\"reviews\") or 0, s[0]))\n else:\n best_conf, best_sig, best = max(scored, key=lambda s: s[0])","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|siege|social|bureau|head|office|\"\n r\"les|le|la|de|du|des|et|and)\\b\")\n\n# types Google d'une fiche d'ENTREPRISE (vs fiche d'un immeuble précis)\n_CORP_TYPES = re.compile(\n r\"agence immobili|gestion immobili|property management|real estate|\"\n r\"societe de gestion|immobilier\", re.I)\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n if _CORP_TYPES.search(cand.get(\"type\") or \"\"):\n signals[\"fiche_entreprise\"] = cand.get(\"type\")\n signals[\"type_fiche\"] = cand.get(\"type\")\n signals[\"nombre_avis\"] = cand.get(\"reviews\")\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # plusieurs fiches quasi équivalentes (chaîne avec une fiche par\n # immeuble) : préférer la plus représentative (le plus d'avis)\n near = [s for s in scored if s[0] >= top - 0.05]\n best_conf, best_sig, best = max(\n near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[{"oldStart":157,"oldLines":11,"newStart":157,"newLines":19,"lines":[" best, best_conf, best_sig = None, 0.0, {}"," if scored:"," top = max(s[0] for s in scored)","- # plusieurs fiches quasi équivalentes (chaîne avec une fiche par","- # immeuble) : préférer la plus représentative (le plus d'avis)","- near = [s for s in scored if s[0] >= top - 0.05]","- best_conf, best_sig, best = max(","- near, key=lambda s: (s[2].get(\"reviews\") or 0, s[0]))","+ # une chaîne a souvent une fiche Google PAR IMMEUBLE en plus du","+ # siège : parmi les candidats plausibles (confiance proche du","+ # meilleur ET au-dessus du seuil), préférer la fiche d'entreprise","+ # puis la plus représentative (le plus d'avis)","+ near = [s for s in scored","+ if s[0] >= max(SEUIL_CONFIANCE, top - 0.15)]","+ if near:","+ best_conf, best_sig, best = max(","+ near, key=lambda s: (bool(_CORP_TYPES.search(","+ s[2].get(\"type\") or \"\")),","+ s[2].get(\"reviews\") or 0, s[0]))","+ else:","+ best_conf, best_sig, best = max(scored, key=lambda s: s[0])"," now = time.time()"," if best is None or best_conf < SEUIL_CONFIANCE:"," con.execute("]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6c43eff6-ab18-49c5-876f-6466c3a67c9b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL71FVcFQDPtW73Egty","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VBX338cQpDJmxE18wHvuTx","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import managers, db\ncon = db.connect()\nmid = con.execute(\\\"SELECT id FROM managers WHERE source_id='logisco'\\\").fetchone()['id']\ncon.execute('DELETE FROM manager_reviews WHERE manager_id=?', (mid,))\ncon.execute('UPDATE managers SET resolved_at=NULL, review_stats=NULL, last_synced_at=NULL, gmaps_place_id=NULL, gmaps_data_id=NULL WHERE id=?', (mid,))\ncon.commit()\nprint('resolve:', managers.resolve(mid, con))\nr = dict(con.execute('SELECT gmaps_name, gmaps_address, gmaps_rating, gmaps_reviews, match_confidence, match_signals FROM managers WHERE id=?', (mid,)).fetchone())\nprint(r)\nprint('sync (5 pages):', managers.sync_reviews(mid, con, max_pages=5))\nimport json\nstats = json.loads(con.execute('SELECT review_stats FROM managers WHERE id=?', (mid,)).fetchone()['review_stats'])\nprint({k: stats.get(k) for k in ('n','moyenne','distribution','pct_negatif','pct_positif','moyenne_12m','n_12m','tendance','plaintes_frequentes','avec_reponse_proprietaire')})\nprint('themes:', list((stats.get('themes') or {}).items())[:6])\nprint('fiche:', json.dumps(managers.fiche('logisco', con), ensure_ascii=False)[:600])\ncon.close()\n\" 2>&1 | grep -v cede959","description":"Full re-test: resolve HQ + paginated sync + fiche","timeout":300000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":749,"cache_read_input_tokens":96981,"cache_creation":{"ephemeral_5m_input_tokens":749,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"152e7802-7241-4578-bb72-38f113629911"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VBX338cQpDJmxE18wHvuTx","type":"tool_result","content":"resolve: {'ok': True, 'gmaps_name': 'Logisco - Siège social', 'confiance': 1.0}\n{'gmaps_name': 'Logisco - Siège social', 'gmaps_address': '950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7', 'gmaps_rating': 3.6, 'gmaps_reviews': 395, 'match_confidence': 1.0, 'match_signals': '{\"similarite_nom\": 1.0, \"meme_domaine_web\": \"logisco.com\", \"ville_correspondante\": \"Québec\", \"fiche_entreprise\": \"Agence immobilière\", \"type_fiche\": \"Agence immobilière\", \"nombre_avis\": 395}'}\nsync (5 pages): {'ok': True, 'nouveaux': 8, 'pages': 1, 'total': 8}\n{'n': 8, 'moyenne': 1.5, 'distribution': {'1': 7, '2': 0, '3': 0, '4': 0, '5': 1}, 'pct_negatif': 88, 'pct_positif': 12, 'moyenne_12m': 1.5, 'n_12m': 8, 'tendance': None, 'plaintes_frequentes': ['service client', 'communication', 'entretien', 'bail et location'], 'avec_reponse_proprietaire': 7}\nthemes: [('service client', {'mentions': 5, 'negatif': 5, 'positif': 0}), ('communication', {'mentions': 4, 'negatif': 4, 'positif': 0}), ('entretien', {'mentions': 2, 'negatif': 2, 'positif': 0}), ('bail et location', {'mentions': 2, 'negatif': 2, 'positif': 0}), ('stationnement', {'mentions': 1, 'negatif': 1, 'positif': 0}), ('personnel', {'mentions': 1, 'negatif': 1, 'positif': 0})]\nfiche: {\"source_id\": \"logisco\", \"nom\": \"Logisco\", \"site_web\": \"https://logisco.com/fr\", \"telephone\": \"+1 833-564-4726\", \"annonces_actives\": 159, \"version\": \"gestionnaire-1.0\", \"google_maps\": {\"nom\": \"Logisco - Siège social\", \"adresse\": \"950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7\", \"note\": 3.6, \"nombre_avis\": 395, \"place_id\": \"ChIJK8TdTNWTuEwR2VoogCXlNu8\", \"confiance_association\": 1.0, \"signaux\": {\"similarite_nom\": 1.0, \"meme_domaine_web\": \"logisco.com\", \"ville_correspondante\": \"Québec\", \"fiche_entreprise\": \"Agence immobilière\", \"type_fiche\": \"Agence immobilière\", \"nombre_avis\": 395}, \"s","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"97c0427a-c537-4d85-a803-334b11b73b93","timestamp":"2026-08-24T22:04:07.716Z","tool_use_result":{"stdout":"resolve: {'ok': True, 'gmaps_name': 'Logisco - Siège social', 'confiance': 1.0}\n{'gmaps_name': 'Logisco - Siège social', 'gmaps_address': '950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7', 'gmaps_rating': 3.6, 'gmaps_reviews': 395, 'match_confidence': 1.0, 'match_signals': '{\"similarite_nom\": 1.0, \"meme_domaine_web\": \"logisco.com\", \"ville_correspondante\": \"Québec\", \"fiche_entreprise\": \"Agence immobilière\", \"type_fiche\": \"Agence immobilière\", \"nombre_avis\": 395}'}\nsync (5 pages): {'ok': True, 'nouveaux': 8, 'pages': 1, 'total': 8}\n{'n': 8, 'moyenne': 1.5, 'distribution': {'1': 7, '2': 0, '3': 0, '4': 0, '5': 1}, 'pct_negatif': 88, 'pct_positif': 12, 'moyenne_12m': 1.5, 'n_12m': 8, 'tendance': None, 'plaintes_frequentes': ['service client', 'communication', 'entretien', 'bail et location'], 'avec_reponse_proprietaire': 7}\nthemes: [('service client', {'mentions': 5, 'negatif': 5, 'positif': 0}), ('communication', {'mentions': 4, 'negatif': 4, 'positif': 0}), ('entretien', {'mentions': 2, 'negatif': 2, 'positif': 0}), ('bail et location', {'mentions': 2, 'negatif': 2, 'positif': 0}), ('stationnement', {'mentions': 1, 'negatif': 1, 'positif': 0}), ('personnel', {'mentions': 1, 'negatif': 1, 'positif': 0})]\nfiche: {\"source_id\": \"logisco\", \"nom\": \"Logisco\", \"site_web\": \"https://logisco.com/fr\", \"telephone\": \"+1 833-564-4726\", \"annonces_actives\": 159, \"version\": \"gestionnaire-1.0\", \"google_maps\": {\"nom\": \"Logisco - Siège social\", \"adresse\": \"950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7\", \"note\": 3.6, \"nombre_avis\": 395, \"place_id\": \"ChIJK8TdTNWTuEwR2VoogCXlNu8\", \"confiance_association\": 1.0, \"signaux\": {\"similarite_nom\": 1.0, \"meme_domaine_web\": \"logisco.com\", \"ville_correspondante\": \"Québec\", \"fiche_entreprise\": \"Agence immobilière\", \"type_fiche\": \"Agence immobilière\", \"nombre_avis\": 395}, \"s","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"da3cc991-e290-4530-99af-d23543c056ce","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL7nRqywFyKvVPXcaXo","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS1QYKpwEIERgCKkBw6sODtgL7TQK/ULLkwGe7PHjBaYLwRDgxpKh5XdOWeLjtm73NsOARsspOzYKOAGJPzOZnEm7umtwwkVl29hyEMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHe/7LUBrABAhIMKpEaoQzScBLi/Ky/GgzDPPp4mZA7+wCKDrgiMG81c7qvfQBwZ+bO41r4S+B0LjEQVCmqw1d/s5zbrylnR7nY2S6Qo68SKhkKYv9iKiraBOx6YXD4uxQtjD0Y5C8GWaIcRC6NhNAAYN2YTrd6ediwKyOZibVjsO29njBlUOZJZsw/PDUvtvH55R8oEG0hcnz1tgehRhH7KWjdvqnPZi18LB22xZQSEe+xFMFuqXHBD72kvjnXNmeo2uTXYMqQQm437KiH0PVFS6M9LuY43QjtBuqxCV/QL4tLlflI1ME1ghR29vK6lcfiiO1565qBx9gfqTH7Z2dd2izILK/QiEM1vv/6bYWvkmvQskDcSuu6Rk5Yr2TrprlYm5eT9blsIt4msUPrRuMrx9VdZrLnHVd0USsKmf65YZfUYx+vv2PWHlw2vY9ntFIA2Jwbm6Rm23n+7FpP5uxLzqEkLw84n5EpYklj78vGoQXpPjwfoVd/PS4iqARzEaTy3aFkdE8hvYBKLm3WdOlCDZY/s7PGR7o0xfiimBBWiE4zYK6JQj5zMK3oXDDZkzfM15JSvUmb4bAbjWupz1trTa1U7sLHaJiViUq+j6tjcj6A0GpD5KYAcangb5AQqazshrzhLJjkZuSjNZEHd+yMjLuZEYahgQLEq6TEt1TXiDgugMRWHYoEuQFSPzcK/6395Qq9qJ5mFFhQCBnsaqGelzJl1eh0CG2aBLo9caZlaK57tpwwKCRfquHNyHA730PdutgMX5AQP+yRoW1OCYdt08kFq0Fa0CI1YjNytudbI5Am7m4SkH4qZx/xo+3Zsl6mHdydwfNJnUEnUXGaDJHPyYzp66BV4VWp1P+U9Y96oxuKqrMd6G7X3sxiKgsjsH4Aez2qbP/weeNZNCsGD0hRjel0GAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1664,"cache_read_input_tokens":97730,"cache_creation":{"ephemeral_5m_input_tokens":1664,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2548d60b-c37d-48dc-a395-cec85c99a99c"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL7nRqywFyKvVPXcaXo","type":"message","role":"assistant","content":[{"type":"text","text":"Le siège est maintenant choisi. Mais la pagination s'arrête à 1 page (8 avis sur 395) — je vérifie le token de pagination dans la réponse brute."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1664,"cache_read_input_tokens":97730,"cache_creation":{"ephemeral_5m_input_tokens":1664,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3429cd4e-5a74-4e2b-a509-3ac1ef870d21"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL7nRqywFyKvVPXcaXo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0196YVfcr2C4hhsZ33ai4Q6Q","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import serpapi_client\nd = serpapi_client.google_maps_reviews('0x4cb893d54cddc42b:0xef36e52580285ad9')\nprint('keys:', list(d.keys()))\nprint('n reviews:', len(d.get('reviews') or []))\nprint('pagination:', d.get('serpapi_pagination'))\n\" 2>&1 | grep -v cede959","description":"Inspect reviews pagination token","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1664,"cache_read_input_tokens":97730,"cache_creation":{"ephemeral_5m_input_tokens":1664,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0f3b678e-e6dc-4c9b-a86f-2c6b67573f5b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0196YVfcr2C4hhsZ33ai4Q6Q","type":"tool_result","content":"keys: ['search_metadata', 'search_parameters', 'place_info', 'topics', 'reviews']\nn reviews: 8\npagination: None","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a40463a6-7a94-43db-a746-b28af3f80a21","timestamp":"2026-08-24T22:04:17.886Z","tool_use_result":{"stdout":"keys: ['search_metadata', 'search_parameters', 'place_info', 'topics', 'reviews']\nn reviews: 8\npagination: None","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"db1979e1-98a6-4d62-b309-63a0615f656b","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL8XooMBPBAHjs5Ly1f","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISrgUKpwEIERgCKkDtcLH3EtxHu2mHkiiszG6rmv4KMN2iAViZjD5wLhqO1JxIMvSzU5begWU8kbfS5xFxxBQK8TLLcVlNPTz4oDHLMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHo/7LUBrABAhIM/d54KyPM/+w5bcbJGgz87jYQaE5AK3wQ0cEiMJdXA+QkUHMe3QgPxYEDvLvIPrKrDibG9kD4iORPChyTyKAqSotNurARZ6NGdhdr1CqzAws86ReosxG3AzuTnw3YqIeIB39YgS3t/zkV6035nrvCrMEbH5a0BeT7J9uJeHojFSgd0Zubp0+OTVg1NBHW246HhpW0mGVO8ClZs6EPXRijt508y4DM4VTEDIWkRNiaKmWeLVNtIPxxbCQPFRp1pWnVoc+onMsMY5RkrpQO5jpWhToiNo9D2h+0HJTUzNl3vsCsn+bnJ8s4bh8OdSdgjvoPnLCdlx7/XjdKAzybVUahySUQNntWjspluyVd0PjTCZBsOPFJVVz/kCUFCv0MiQnDEqmlRslDrzwQQ9fjOq/SzIi4yrPM1JtyGAP4cy7BmC95EoC+fH4r17bHHC7aYH96pOUEXyA1TB0qXJ3w5Pasmx1MUeNtfBDnMk5FCNIdWKjEOckMDRxtN0OVmSIadk8Mg7eC2cBt3TcXoGfYP47xnQhnZrQkVEGaoPF/SVazWkdRgXK17wXetrORISFdLvZ8SyyGIPbCWa3Yf7Gt8S8RmWqkVg1WTaR85oSvjDf1tvP+kcB6iI8ltKsh0VCuxH/DMPqYYGbIzsOQ7pSUt1wZsPzPXb16Vg830vpRcnyUOJ9lLBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":596,"cache_read_input_tokens":99394,"cache_creation":{"ephemeral_5m_input_tokens":596,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d9cf6afe-7ee7-4aff-a24f-f6fe148729b9"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL8XooMBPBAHjs5Ly1f","type":"message","role":"assistant","content":[{"type":"text","text":"Pas de token retourné. J'essaie avec `num=20` (paramètre requis par SerpApi pour la pagination)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":596,"cache_read_input_tokens":99394,"cache_creation":{"ephemeral_5m_input_tokens":596,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8ee601ce-ee1c-4bd6-89f1-25f99b65342d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL8XooMBPBAHjs5Ly1f","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Tsrtuo2AtDFdyHfbQVTgwX","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import serpapi_client\nd = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr','sort_by':'newestFirst','num':20})\nprint('n reviews:', len(d.get('reviews') or []))\nprint('pagination:', d.get('serpapi_pagination'))\n\" 2>&1 | grep -v cede959","description":"Test pagination with num=20","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":596,"cache_read_input_tokens":99394,"cache_creation":{"ephemeral_5m_input_tokens":596,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3838f196-c63c-4e70-9b24-14413bf425d9"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Tsrtuo2AtDFdyHfbQVTgwX","type":"tool_result","content":"Traceback (most recent call last):\n File \"\", line 3, in \n d = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr','sort_by':'newestFirst','num':20})\n File \"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py\", line 63, in search\n raise SerpApiError(f\"SerpApi HTTP {r.status_code}\")\nlouka.serpapi_client.SerpApiError: SerpApi HTTP 400","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9f39de96-a3f7-49a1-91fa-8edefec9b43c","timestamp":"2026-08-24T22:04:26.407Z","tool_use_result":{"stdout":"Traceback (most recent call last):\n File \"\", line 3, in \n d = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr','sort_by':'newestFirst','num':20})\n File \"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py\", line 63, in search\n raise SerpApiError(f\"SerpApi HTTP {r.status_code}\")\nlouka.serpapi_client.SerpApiError: SerpApi HTTP 400","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"25f80c12-4a11-43b2-9b44-8b40416bcc54","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL9ACJzg7mciHGyS1Bx","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISywQKpwEIERgCKkC8TGFb/bK8BLsOA9nl7G0IwvubP0G/iqDbZUBc1pEtTRkK4H+vbItukce7xTw0f/lUiJNYs8L02cPe0FfLIvqeMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHu/7LUBrABAhIMg5Q3WDaedXPlME/rGgxdxEz2IjEhtOst96AiMEm4bB814QrYxHtNTI8o65v3g5bX8EoiDMPQRfGD9TBbH1sdSSmIZYtWR3faAQtR+irQAu7XT1Ue0ZyIWIu8HO3XsmKhjOQyIdBtbzncjE6hTNUNQY70KRFLBoo57rQ3oP0P8+QHiT+fLkuUq35ttybuYW/kZ86hcOv49AFOiUkxiluoyOOBkIxANXimy5Af75gTJ3mWQ3BLu40vQ72FC75MtbhkVgge3qVKehOTJxj4QJwkWn3L3x8wPViIJiHYIZZwbPlfuhnpnaOjdJ5XI+eizN8v08JOh7YAAqIRYpzOO+CItf2kJroH3cAFo3ZatekZdXL+qPr+TVuwwsf0ndIoJshskqNj+wgVVCGh4uOhMf22iEcpdfvj4oq6ZB2AhFreodFKaZgJ3jYRIGgH8XWgb0LMY/I4GEznetWtHYGErpvgpo7dzacYjaIlzGUlND3kApBppQ3yBwr/woYAA4LBt3W8NYJeBNSDRfuY7hKJ7GwWeoTBW/GYeElNhrKsqmsdlhgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":686,"cache_read_input_tokens":99990,"cache_creation":{"ephemeral_5m_input_tokens":686,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3fb08b43-11ec-4780-83a6-cc6db88d56e0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL9ACJzg7mciHGyS1Bx","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DwWmfdaCebXpuMiDon6XRp","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import serpapi_client\nd = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr'})\nprint('sans sort_by — n:', len(d.get('reviews') or []), 'pagination:', d.get('serpapi_pagination'))\nd2 = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr','sort_by':'newestFirst'})\nprint('avec sort_by — n:', len(d2.get('reviews') or []), 'pagination:', d2.get('serpapi_pagination'))\nprint('meta:', d2.get('search_metadata',{}).get('status'), '| params:', d2.get('search_parameters'))\n\" 2>&1 | grep -v cede959","description":"Compare pagination with/without sort_by","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":686,"cache_read_input_tokens":99990,"cache_creation":{"ephemeral_5m_input_tokens":686,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c684189c-8ff7-4edc-ab71-53f8f8ce3832"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DwWmfdaCebXpuMiDon6XRp","type":"tool_result","content":"sans sort_by — n: 8 pagination: None\navec sort_by — n: 8 pagination: None\nmeta: Success | params: {'engine': 'google_maps_reviews', 'data_id': '0x4cb893d54cddc42b:0xef36e52580285ad9', 'hl': 'fr', 'sort_by': 'newestFirst'}","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d56a67e0-3bf9-4a72-903c-eff9d51e78e9","timestamp":"2026-08-24T22:04:36.314Z","tool_use_result":{"stdout":"sans sort_by — n: 8 pagination: None\navec sort_by — n: 8 pagination: None\nmeta: Success | params: {'engine': 'google_maps_reviews', 'data_id': '0x4cb893d54cddc42b:0xef36e52580285ad9', 'hl': 'fr', 'sort_by': 'newestFirst'}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8342b3bd-9590-4ff7-9e35-7dd8737d422c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL9tcDmQJLr2HV73iwP","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISgwcKpwEIERgCKkCHiX6mHKhEAL83Z8D77E3yHR9Ku2kiSP6KLccVjpQSRQi0zIpCGb2jNwx68PIv3Dg65AkUon1JWwk36iek5jvwMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH6/7LUBrABAhIMlNrCG4COkK+H4tw+GgwEf7vnB8gAXVFTu7ciMFqTGexlYJodWdPTz43pzNsMSnqGh8OHG0l/xKe2WExcMqmShMy8qCajGyR4qaulFSqIBVE2k9453A7gcdN1z407fxuxdyz9bN4HawaKE/VM8sWdVPSEHYl2oPm5dpzbHHB2Q7Ecevc9ObNuanUM/6PjEEiQK7ZM2GvQDc5ACr8wQ6VbNAzTHbw9bSIYUabDf9JouJro79ByYkc63j0+TXxjji4X6P6ppASPnsTRZQMdBW5ZuBCgkBsRyn6z5SBTbqFoaslHnGdH66kizJoeVxJmqLP35XLjcxo6quHfuBo9RvF56bkqi4BGeMxzb/0BiDHzXNvjLrn38eqlRQkxNGrjBa4+Mz97Qumb//f/jNcI+Y97d6Q1i7Aq7aLYQ2mWucgH/rWRaYWmrdAuRhr0ZxJj2j8P0+et7WpcoG5M7Y2DRyIKOVCI+yWHLL09tJBZixSqOe9+PYgWnDEb2ZbWDwmJg3D/AVvcBmLb0hstkc25ME5NxfYwM9k4JITBGWWe1cOhjzsaPW1jgCuw9Z2IAHHQVg8RO3kxKW+EnTcbuq3tw3rQUkmLA/L0h8MxZnzVY7bFC+N/Jxk/VGVzv/6I+vdSDYaWsnVPqHDPtQu5w0OVqHXGxiZNipNPd5Px+Kv8aPSCvYZqrA6lxdGWvS4WAPiQsJ57alI2QRZ4Y8aSYxw5uv/nKfPFubGwN6tz/IGrrExk9dcSjsFSTvthVUr29Eq5fvtZmXzQ2hk2GanzfKWlx1m2Fw8vats8Hv8NHNUWdTW0/PRe3SQNDxPEFeP1Mvl3juNzqa+LXnI9VAcY0X8XO7DoqbBTWBKdDWha3mIsVnxwqxTmqyhtRrUopz4QJnv8PXUv7ZsR9SqRg7td6FDIYo8/gnbGRqm9tevoWiPq4S+82Sgohjxjngx9SQCxDJsDqdUAwf+jSFgFsxgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":668,"cache_read_input_tokens":100676,"cache_creation":{"ephemeral_5m_input_tokens":668,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7860af03-c6d5-4b6e-82dc-cd64eca1657a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNL9tcDmQJLr2HV73iwP","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01LjNWpfZbFQpyxtZsVKYYjz","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nimport json\nfrom louka import serpapi_client\nd = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr','sort_by':'newestFirst'})\nprint('place_info:', json.dumps(d.get('place_info'), ensure_ascii=False))\nprint('topics:', json.dumps((d.get('topics') or [])[:3], ensure_ascii=False))\nrv = d.get('reviews')[0]\nprint('review keys:', list(rv.keys()))\nprint('dates:', rv.get('date'), '|', rv.get('iso_date'))\n\" 2>&1 | grep -v cede959","description":"Inspect place_info and review shape","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":668,"cache_read_input_tokens":100676,"cache_creation":{"ephemeral_5m_input_tokens":668,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4fec8582-e1a9-41ed-a7fa-951f7b3e7510"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01LjNWpfZbFQpyxtZsVKYYjz","type":"tool_result","content":"place_info: {\"title\": \"Logisco - Siège social\", \"address\": \"950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7, Canada\", \"rating\": 3.6, \"reviews\": 395, \"type\": \"Agence immobilière\"}\ntopics: [{\"keyword\": \"logement\", \"mentions\": 25, \"id\": \"/m/025tg6m\"}, {\"keyword\": \"concierge\", \"mentions\": 15, \"id\": \"/m/03pqt3\"}, {\"keyword\": \"immeuble\", \"mentions\": 13, \"id\": \"/g/1227pvxm\"}]\nreview keys: ['position', 'link', 'rating', 'date', 'iso_date', 'iso_date_of_last_edit', 'source', 'review_id', 'user', 'snippet', 'extracted_snippet', 'likes']\ndates: il y a 5 jours | 2026-08-19T01:06:10Z","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"75a4ecd2-9ea7-4262-b137-d44296b9c1a4","timestamp":"2026-08-24T22:04:46.338Z","tool_use_result":{"stdout":"place_info: {\"title\": \"Logisco - Siège social\", \"address\": \"950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7, Canada\", \"rating\": 3.6, \"reviews\": 395, \"type\": \"Agence immobilière\"}\ntopics: [{\"keyword\": \"logement\", \"mentions\": 25, \"id\": \"/m/025tg6m\"}, {\"keyword\": \"concierge\", \"mentions\": 15, \"id\": \"/m/03pqt3\"}, {\"keyword\": \"immeuble\", \"mentions\": 13, \"id\": \"/g/1227pvxm\"}]\nreview keys: ['position', 'link', 'rating', 'date', 'iso_date', 'iso_date_of_last_edit', 'source', 'review_id', 'user', 'snippet', 'extracted_snippet', 'likes']\ndates: il y a 5 jours | 2026-08-19T01:06:10Z","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f3496ca3-4688-48dc-9055-1eaecee35c11","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLAdPDKaMYNV2qKgpbL","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS6QYKpwEIERgCKkBuso353KNilH35njN+IixE4xNnGW2eTeNmY6xCxOT4KlwPf2sXpdsVXIbsQIVmxSsinzbPYqkwfjRRirv6IOCyMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGGgLPUBrABAhIMLbYTmGs9QvGjjJwGGgwHRSGaNMXCRaXdgmMiMIoWPtoOZH3mDOdlI601Ez4MhVuJmuAToJbvvWmG/jVMuCPVtb217rGR1HtRqmk2eSruBHqK/UHixddXp48w6i/oKiotTqVQOjFQBJlrFmljNPxSp8WLuziVb4CNei0WMSKJ5QPRer6At2BxZ/GQaZQjKJG1cRlKBT11sGqF33sl+esrxtzH4SsZke6CFocwiY/UTKah8FCvIjm2jqzgCQb3tRnh2Sr9mhNN7BFhxYgTb+R6oaE/pZO7heDaaD74HMhQDkDdvpdZvVynrtX949znoChqUm93aHXFciHUJw5V5WpeKVNXvbZsuHjwNTrq14rYfRg0GkYbaxJx1wz6Jh+CR7DCv2oCa9Q6vjgb4zVE8FIwKA8oT5AGDPT9ufGwJlRpJtBPQaqFz1cckjW9hPQiKRN3Yz1ULMGp/FshVn1YDzX8QtlEKrYR8YyvhIzRb88gydixkOtUCWU3pp57f0BmFGjNv09JUmO1uh1TngBPGAPcWcSuYwAnMWSZXlLfOg5opX4OAVDKdj1QNBdU1gdNMSKgMvxwHhuuUrekAqhVLz1jpqJgwqXQqsnbvyeiA8I21Vd+uAjddrXTaljQRAJBn2KTSz+3E6Bnvr4WQ2TafZawbT69JyDHVP8xgGZilBD2d1ArCiAx49ivXdOTMwkZpAP2ItzGvOw8iwffZIG/KJQOi/V5gts2VFhcXRZRejrvRazwiact7fT6ez7vW9wecPvtafUvMiW1Y6f8TImIo+FmTsWvznAavWn3h4kx+fnyzctF/ousxbsLWU51VDrP9frD2GhfzeuJactIi/XBWmc97Ph88vgiw9oDG2EySmYSzOcQ49mQqd7hFGeNifWtDQEIQe8PbFAAKhCD9bZQS6NsjZh0a9QHTRMvnookJjYYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1458,"cache_read_input_tokens":101344,"cache_creation":{"ephemeral_5m_input_tokens":1458,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cdf84cb9-88af-433e-ae72-cd45772019f7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLAdPDKaMYNV2qKgpbL","type":"message","role":"assistant","content":[{"type":"text","text":"La fiche est la bonne (395 avis) mais SerpApi ne renvoie ni token ni plus de 8 avis. J'essaie avec `place_id` au lieu de `data_id`."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1458,"cache_read_input_tokens":101344,"cache_creation":{"ephemeral_5m_input_tokens":1458,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"17c12bc8-6093-4178-b58e-86d595945468"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLAdPDKaMYNV2qKgpbL","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MV6PaL2yo7e3KhpBoPcsc8","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import serpapi_client\nd = serpapi_client.search({'engine':'google_maps_reviews','place_id':'ChIJK8TdTNWTuEwR2VoogCXlNu8','hl':'fr','sort_by':'newestFirst'})\nprint('place_id — n:', len(d.get('reviews') or []), '| pagination:', d.get('serpapi_pagination'))\nd2 = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9'})\nprint('data_id sans hl/sort — n:', len(d2.get('reviews') or []), '| pagination:', d2.get('serpapi_pagination'))\n\" 2>&1 | grep -v cede959","description":"Test pagination via place_id and bare params","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1458,"cache_read_input_tokens":101344,"cache_creation":{"ephemeral_5m_input_tokens":1458,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7474a8f2-e367-4a3d-a232-61301580d707"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bhzq2pm2b","tool_use_id":"toolu_01MV6PaL2yo7e3KhpBoPcsc8","description":"Test pagination via place_id and bare params","task_type":"local_bash","uuid":"1cfa98f1-b983-4ba9-9aa6-39614c5c7549","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bhzq2pm2b","tool_use_id":"toolu_01MV6PaL2yo7e3KhpBoPcsc8","status":"completed","output_file":"","summary":"Test pagination via place_id and bare params","uuid":"21d46f05-a52a-4608-8599-16e7870e7634","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MV6PaL2yo7e3KhpBoPcsc8","type":"tool_result","content":"place_id — n: 8 | pagination: None\ndata_id sans hl/sort — n: 8 | pagination: None","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"daad7a8b-85ab-43a9-b2d1-fe9d2c15de87","timestamp":"2026-08-24T22:05:01.897Z","tool_use_result":{"stdout":"place_id — n: 8 | pagination: None\ndata_id sans hl/sort — n: 8 | pagination: None","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"5f0c1022-1e97-42ae-a16f-ff56500ceeea","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLBnPiyjtGK7C91o5RW","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISvwQKpwEIERgCKkCTsCxEy9SPA37OMJiu9ZcYnmN3mUU2XfZ0APyMx0+ODdvEI8ceQAeg6xJ6Rqp2gB5xXisw3ymEz1IFLoVnd23+Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGTgLPUBrABAhIMI0nxiotD2vrvNTvlGgxXc1/hOO4maZEbSDgiMHIkuC4CYsZo2lTGYqP47EJG2wyX8YWRKjNuMiF9wRB0Meto5qzidDpmE7H6EmQXpyrEAp9afAgzbMOErvNwZ95DhUU7OPFfvymPiprZiBz8DZQLjwjUQdmT8AsbTR4mqy174495xsoaQUw7IeFRF+T9PEyQSkWOyRMXnyqHfwIEdOTxhWFNf7rd8DX++zIFkpK6mObgQK+QDIrKZoUWc6csMD/we0VuCuSiHAN4jLOagQy+Yspb91he/0PAI3afATrOSa74mbmzbtWrBKLyFmxt/OikB3kGFtoizFkxqM6TxLc2hFPHlsDdmG2QEh1r4i1ON857XCn4nTxlXaAfMvtyGEWsgQu9zAd1OHdsSuaeJiFfcOiGGQlWgU6Q66DhPJmxMdMnP3oBHY5UGMOHO6eVNc6Mi0MVQHPsNvRIxBj0xYZYU3O/ux6FZJclU9lH/a1BOEIKGXgSAbWQbCJFHMFpW+NeHQV3nZbdRxt+lBwhmP9KWri42RgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":715,"cache_read_input_tokens":102802,"cache_creation":{"ephemeral_5m_input_tokens":715,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"60bfb3e8-09a1-4350-af1c-7f762a233b9b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLBnPiyjtGK7C91o5RW","type":"message","role":"assistant","content":[{"type":"text","text":"Toujours 8 avis maximum. Je vérifie si c'est une limitation connue de SerpApi."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":715,"cache_read_input_tokens":102802,"cache_creation":{"ephemeral_5m_input_tokens":715,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"99cea2fe-7252-4e4e-bf63-ae32cc50a47e"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLBnPiyjtGK7C91o5RW","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01F73Kh8caq7iTJsBxCDbaYj","name":"ToolSearch","input":{"query":"select:WebSearch,WebFetch","max_results":2},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":715,"cache_read_input_tokens":102802,"cache_creation":{"ephemeral_5m_input_tokens":715,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a92e0b5f-8ca5-4cd2-bedd-47025442dbde"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01F73Kh8caq7iTJsBxCDbaYj","content":[{"type":"tool_reference","tool_name":"WebSearch"},{"type":"tool_reference","tool_name":"WebFetch"}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"54d196ab-692a-42a4-b3b2-174245380494","timestamp":"2026-08-24T22:05:07.900Z","tool_use_result":{"matches":["WebSearch","WebFetch"],"query":"select:WebSearch,WebFetch","total_deferred_tools":17}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8d58d47a-4c49-41e0-83d3-a21475c486ec","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLCDkxZBAKVU7aZVZgm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SWfMc9BENEAcoYcoe6FztV","name":"WebSearch","input":{"query":"SerpApi google_maps_reviews next_page_token missing only 8 reviews 2026"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1621,"cache_read_input_tokens":103517,"cache_creation":{"ephemeral_5m_input_tokens":1621,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e38799ad-ff6c-414c-a352-aa5eb5abd896"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SWfMc9BENEAcoYcoe6FztV","type":"tool_result","content":"Web search results for query: \"SerpApi google_maps_reviews next_page_token missing only 8 reviews 2026\"\n\nLinks: [{\"title\":\"[Google Maps Reviews API] Pagination and filter query searches failing · Issue #3939 · serpapi/public-roadmap\",\"url\":\"https://github.com/serpapi/public-roadmap/issues/3939\"},{\"title\":\"Scrape Google Maps Business Reviews with SerpApi's Google Sheets Extension [No Code]\",\"url\":\"https://serpapi.com/blog/scrape-google-maps-business-reviews-with-serpapis-google-sheets-extension-no-code/\"},{\"title\":\"[Google Maps Reviews API] Pages beyond a certain point returning empty · Issue #2794 · serpapi/public-roadmap\",\"url\":\"https://github.com/serpapi/public-roadmap/issues/2794\"},{\"title\":\"Google Maps Reviews API - SerpApi\",\"url\":\"https://serpapi.com/google-maps-reviews-api\"},{\"title\":\"Scraping Google Maps Place Reviews Results using SerpApi\",\"url\":\"https://serpapi.com/blog/using-google-maps-reviews-api-from-serpapi/\"},{\"title\":\"GitHub - serpapi/google-maps-reviews-scraper: Google Maps Reviews Scraper tool to scrape maps reviews results with simple API. Get reviewer information alongside the review text and response. · GitHub\",\"url\":\"https://github.com/serpapi/google-maps-reviews-scraper\"},{\"title\":\"Scrape Google Maps data and reviews using Python\",\"url\":\"https://serpapi.com/blog/scrape-google-maps-data-and-reviews-using-python/\"},{\"title\":\"How To Scrape Google Maps Contributor Reviews\",\"url\":\"https://serpapi.com/blog/how-to-scrape-google-maps-contributor-reviews/\"},{\"title\":\"Get Google Maps Listing Reviews | SerpApi | One - WithOne AI\",\"url\":\"https://www.withone.ai/knowledge/serp-api/conn_mod_def::GJ6ZysNkteM::VJ8RlcjxS527COWukVFhHA\"}]\n\nBased on the search results, I found relevant information about SerpApi's Google Maps Reviews API and issues with `next_page_token`:\n\nIn June 2026, a customer reported an increase in failed searches with SerpApi's Google Maps Reviews API, and after reviewing further, it was found that only searches with next_page_token fail. This was tracked as issue #3939 in SerpApi's public roadmap.\n\nAdditionally, when using certain filters or parameters, the API always returns 8 results, which could be related to the \"only 8 reviews\" aspect of your query.\n\nThere was also a previous related issue where a customer reported some businesses with more than 8k reviews, but they weren't able to get all of the listed reviews, with only approximately 5k reviews being scrapable.\n\nFor getting more than 10 reviews, you need to use the pagination parameter next_page_token to extract the next page of review results.\n\nIf you're experiencing specific issues with the `next_page_token` parameter or missing reviews, it appears this may be a known issue that SerpApi has been tracking and working on resolving.\n\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"650ac671-59c1-46b0-ac56-03bc0752a463","timestamp":"2026-08-24T22:05:17.402Z","tool_use_result":{"query":"SerpApi google_maps_reviews next_page_token missing only 8 reviews 2026","results":[{"tool_use_id":"srvtoolu_014CFTxWAQwXyUikJ9pMyXeg","content":[{"title":"[Google Maps Reviews API] Pagination and filter query searches failing · Issue #3939 · serpapi/public-roadmap","url":"https://github.com/serpapi/public-roadmap/issues/3939"},{"title":"Scrape Google Maps Business Reviews with SerpApi's Google Sheets Extension [No Code]","url":"https://serpapi.com/blog/scrape-google-maps-business-reviews-with-serpapis-google-sheets-extension-no-code/"},{"title":"[Google Maps Reviews API] Pages beyond a certain point returning empty · Issue #2794 · serpapi/public-roadmap","url":"https://github.com/serpapi/public-roadmap/issues/2794"},{"title":"Google Maps Reviews API - SerpApi","url":"https://serpapi.com/google-maps-reviews-api"},{"title":"Scraping Google Maps Place Reviews Results using SerpApi","url":"https://serpapi.com/blog/using-google-maps-reviews-api-from-serpapi/"},{"title":"GitHub - serpapi/google-maps-reviews-scraper: Google Maps Reviews Scraper tool to scrape maps reviews results with simple API. Get reviewer information alongside the review text and response. · GitHub","url":"https://github.com/serpapi/google-maps-reviews-scraper"},{"title":"Scrape Google Maps data and reviews using Python","url":"https://serpapi.com/blog/scrape-google-maps-data-and-reviews-using-python/"},{"title":"How To Scrape Google Maps Contributor Reviews","url":"https://serpapi.com/blog/how-to-scrape-google-maps-contributor-reviews/"},{"title":"Get Google Maps Listing Reviews | SerpApi | One - WithOne AI","url":"https://www.withone.ai/knowledge/serp-api/conn_mod_def::GJ6ZysNkteM::VJ8RlcjxS527COWukVFhHA"}]},"Based on the search results, I found relevant information about SerpApi's Google Maps Reviews API and issues with `next_page_token`:\n\nIn June 2026, a customer reported an increase in failed searches with SerpApi's Google Maps Reviews API, and after reviewing further, it was found that only searches with next_page_token fail. This was tracked as issue #3939 in SerpApi's public roadmap.\n\nAdditionally, when using certain filters or parameters, the API always returns 8 results, which could be related to the \"only 8 reviews\" aspect of your query.\n\nThere was also a previous related issue where a customer reported some businesses with more than 8k reviews, but they weren't able to get all of the listed reviews, with only approximately 5k reviews being scrapable.\n\nFor getting more than 10 reviews, you need to use the pagination parameter next_page_token to extract the next page of review results.\n\nIf you're experiencing specific issues with the `next_page_token` parameter or missing reviews, it appears this may be a known issue that SerpApi has been tracking and working on resolving."],"durationSeconds":6.224518624999793}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"12fe9261-8356-4141-8cf0-1099d21e955e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLCvJ1YeH1KCLTm8U7P","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019NVMkFnoqX4HmeYtjzj1xK","name":"WebFetch","input":{"url":"https://github.com/serpapi/public-roadmap/issues/3939","prompt":"Summarize this issue about Google Maps Reviews API pagination failing: what is broken, since when, is there a workaround, is it fixed? Report status and dates."},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1197,"cache_read_input_tokens":105138,"cache_creation":{"ephemeral_5m_input_tokens":1197,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"663c96cd-a3af-4e83-893b-3c2cc8a74806"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019NVMkFnoqX4HmeYtjzj1xK","type":"tool_result","content":"# Google Maps Reviews API Pagination Issue Summary\n\n**What's Broken:**\nSearches using `next_page_token` for pagination and filtered queries are failing in the Google Maps Reviews API.\n\n**Timeline:**\nThe issue was opened June 18, 2026, after a customer reported \"an increase in failed searches.\"\n\n**Status:**\nThe issue is marked as closed with labels indicating \"status: completed\" and \"users notified,\" suggesting the problem has been resolved and users were informed of the fix.\n\n**Workaround:**\nNo workaround is mentioned in the issue description.\n\n**Related Issues:**\nThe report notes a possible connection to issue #2818, indicating this may not be an isolated incident.\n\n**Additional Context:**\nTwo specific search inspect links were provided for debugging purposes, and the status page reference suggests SerpAPI tracks this service's reliability publicly."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6a955850-d641-48da-8a9f-f75c9304e973","timestamp":"2026-08-24T22:05:25.358Z","tool_use_result":{"bytes":274722,"code":200,"codeText":"OK","result":"# Google Maps Reviews API Pagination Issue Summary\n\n**What's Broken:**\nSearches using `next_page_token` for pagination and filtered queries are failing in the Google Maps Reviews API.\n\n**Timeline:**\nThe issue was opened June 18, 2026, after a customer reported \"an increase in failed searches.\"\n\n**Status:**\nThe issue is marked as closed with labels indicating \"status: completed\" and \"users notified,\" suggesting the problem has been resolved and users were informed of the fix.\n\n**Workaround:**\nNo workaround is mentioned in the issue description.\n\n**Related Issues:**\nThe report notes a possible connection to issue #2818, indicating this may not be an isolated incident.\n\n**Additional Context:**\nTwo specific search inspect links were provided for debugging purposes, and the status page reference suggests SerpAPI tracks this service's reliability publicly.","durationMs":4094,"url":"https://github.com/serpapi/public-roadmap/issues/3939"}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"51d92dc9-5c39-4b53-b958-8a2eed657724","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLDWBTLVBrX28ZUesSK","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISpgoKpwEIERgCKkA+ZOGRZ3IjWGrLvv1ooP2Zv/FcT5BhZDlbgEQ1gk6grC0OXCXNCX3FPaYSYJG5jgTQAwrj3v+Gf8BJXJhl+AZeMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGvgLPUBrABAhIMa4pXm00rNmU+mAJEGgypubdCmPzAJuD16owiMKmHJPCSccW4FuGMfKAZfu2G1ZBewbnll+pH7HctO2Px736nyVzPx1sboMChHpxFeiqrCM5XTC1THDruTor5VXsBAGbxDcDJ9WR/FQJZR12NIBSf5Xk954wJzMwQMHO2psaOAi4lxz6PwdTBRn0Qz1t+vn3O72521DN8I8swrFHQ6AzlnRUSxBu6GHvtO4gEYP7v4VnTy/BcR8jWJkeWHnYHFAkSAN1NJBEb0pq4Cb9+u7hzGZSexIb8624nmw+VRZgVsQao5tcOcJ/oC5WdUO0sBJpNLtm/soNwhDQCSNamq6BkYOEiOKfKaju8NwmGkosY+7R2xLZOC+wlSUznVUDaNjH3sn/6TtfFt3Gn497SQna+L0QYd2IUjnqtlt0r10Msmzymcz1QKmDUnsTCsYny3+jpji4nT4HaeVqZLf245tx1Eb+N3Huo9Vep7VEc+UL6O0oP0qv5Dqyi1hwyqGXVQWkQZ72vKuEBiJmyearBNdfdEAfcvOKp2CCAPbX9WZFebrAGDRzWDkM1hNVrXVFyk4j9W0F8Wo/719uQhDleB07proikYo+CyWD/1Juj2vKMpsHJuW7gElvFLiSI4WIeEP5/3zfNkWrGu/ud4Y49cRshPqBd+rP9hn7M+XttVBDKLMSVr4MuWxU66VEki54HXEmP4+xNM4U7cjI8dh6+cZCunOSLpLiFgVEjHXCXoNGceNk8CMOW+0FH2zaWd6yYeTzGM46hwI2Eqck7+wVl742PhtFq3AefswALUyorS6xwXCBZZeI+ZXkHFGFHBvvCzhJSTmRxU3u5aiKilTH7OavkA5nHNHkyEBkA/96F7/KYzY9nW6u2WvYPi9m6Abo4RFf/Q40toiJlEpwM3SVls1wAmSTELx+RsSCQUjl5lKqXIPD+0+bJQsvtDA2tzVHGYqjy5LSrW/H/bEdusDuOI2hE8tJbxqU7OAG0GoDGdPfCd4TTvsA+dhfeq2Zr9zFRPgDgFHBC36lDAXd83shJ20GOirQBLrk7ZuARCo3hC80mqNy9QwK3ch8x5JCKIuWkx74FM+oPfFyRyxhx6/02dkoI6vpKpGC8kr5MGLgevht4tkqGi7CKOaV+/7Kj0TytCDisD4YJTIiOK9XZTVBQd0v9IczJoZre91+cb1cdrj5wYX/C0zyHKy4wW1knjWXy479OJ2yEjV5OQ5VQQyGJSu3PYhbudl5xejmAERd27x2nZxXvuIvhFi0Z6D8q3hf8ZNmOzEi4zbWssCL2VBNC0fpU8NkEThKRlmGIEGt5CLLmuy+sYiM4wcyp+tgY7+Fcu9J3QMEU6X7499miHzVJwmsDXq8s91LWIFl6g2AOpcUG5Q+tTgNq3OCt/bgmTvGozI5hY32jez85CpxyxOEqWgq/gI/hFta6FOZQyq9bwcg0aDEIrVeZp4lyP2p9mRnza93tfq+5RceXLzxaU+DH75/uoR3FbFiBb7D+xDqqD+rEZOzwKOrtXXYU7p77GAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":435,"cache_read_input_tokens":106335,"cache_creation":{"ephemeral_5m_input_tokens":435,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"543336a5-ff2a-4ab1-8fc8-a95f90d7b83d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLDWBTLVBrX28ZUesSK","type":"message","role":"assistant","content":[{"type":"text","text":"Le bug SerpApi (juin 2026) est marqué résolu, mais je n'obtiens aucun token. Je vérifie la doc actuelle de l'API."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":435,"cache_read_input_tokens":106335,"cache_creation":{"ephemeral_5m_input_tokens":435,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d2933d49-dc96-4857-a6a7-972a54fe70f8"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLDWBTLVBrX28ZUesSK","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NRYLMmttS3sYVpcQiq2RvY","name":"WebFetch","input":{"url":"https://serpapi.com/google-maps-reviews-api","prompt":"How does pagination work for this Google Maps Reviews API? Which parameters are required to get next pages (next_page_token, num)? Under what conditions is serpapi_pagination.next_page_token returned or absent? Any note about the first page returning only 8 reviews?"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":435,"cache_read_input_tokens":106335,"cache_creation":{"ephemeral_5m_input_tokens":435,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"edab103f-ce01-418c-b17d-0dab198197d1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NRYLMmttS3sYVpcQiq2RvY","type":"tool_result","content":"# Google Maps Reviews API Pagination\n\n## How Pagination Works\n\nThe API uses token-based pagination rather than offset-based. According to the documentation, \"Usage of `start` parameter (results offset) has been discontinued by Google.\"\n\n## Required Parameters for Next Pages\n\nTo retrieve subsequent pages, you need:\n- **`next_page_token`**: Found in the response at `serpapi_pagination -> next_page_token`\n- **`num`** (optional): Controls results per page (1-20, defaults to 10)\n\nHowever, there's an important caveat: \"Parameter cannot be used on the initial page when neither `next_page_token`, `topic_id`, nor `query` is set.\"\n\n## First Page Behavior\n\nThe initial request has a quirk: \"It always returns `8` results\" on the first page when using default parameters, regardless of the `num` setting. To control the number of results on the first fetch, you must include either a `topic_id` or `query` parameter.\n\n## Token Availability\n\nThe `next_page_token` appears in the structured JSON response under `serpapi_pagination`. It's present when additional pages exist; its absence indicates you've reached the final page of results."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"39763031-b7a7-4643-89d9-3c6e8630e1e5","timestamp":"2026-08-24T22:05:40.110Z","tool_use_result":{"bytes":7406,"code":200,"codeText":"OK","result":"# Google Maps Reviews API Pagination\n\n## How Pagination Works\n\nThe API uses token-based pagination rather than offset-based. According to the documentation, \"Usage of `start` parameter (results offset) has been discontinued by Google.\"\n\n## Required Parameters for Next Pages\n\nTo retrieve subsequent pages, you need:\n- **`next_page_token`**: Found in the response at `serpapi_pagination -> next_page_token`\n- **`num`** (optional): Controls results per page (1-20, defaults to 10)\n\nHowever, there's an important caveat: \"Parameter cannot be used on the initial page when neither `next_page_token`, `topic_id`, nor `query` is set.\"\n\n## First Page Behavior\n\nThe initial request has a quirk: \"It always returns `8` results\" on the first page when using default parameters, regardless of the `num` setting. To control the number of results on the first fetch, you must include either a `topic_id` or `query` parameter.\n\n## Token Availability\n\nThe `next_page_token` appears in the structured JSON response under `serpapi_pagination`. It's present when additional pages exist; its absence indicates you've reached the final page of results.","durationMs":3651,"url":"https://serpapi.com/google-maps-reviews-api"}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"605d6571-acb1-49d7-abb9-a95951260d29","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLEbePp7YbdBKZt5hjH","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS9QYKpwEIERgCKkBApT5dOpRg5JMEusvrfjK4W1tSbh9KsNR4W38jjzL4KWFMGw2GyliOaRvZPxPm+d8dDIqJkd6NU4O0a44YBkfrMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG7gLPUBrABAhIMFLA+TjrLClDfvef8GgybHy+1lIfvL/nQFuUiMIa7J+NeAP74N7oEfU0A/+WSl2lMNdRqu4jQeLnjPy+QF2siLxP15TLZO3MsMkWiPyr6BAgyum5lPnTT620fLY39dn8iPaYIu0uj1AviYbTzjMGcfzZ8FvzqCL4AjiQgtezFAVpNhJVKRDLWJWCZGGH9HTij4IynwQ43av3pwduqf6j23081kwfKZCWFC1T2Kxm+8Sa3eOxjs5PehKixgAt4Vg1tm0ZrlAXkkqNnswkk5APfV5YxXhRRmN62MYX1CC1ohDwQ+Ry7DLOwftDiGUUBBmW2XTZqjKTiO3nJzKn9unZi9go5xqnox597Jw+4KmATA+loyAtngDrhVRRD43tk8crG+2HF9h0wQx58/CcwYCDNDn5y5g+9+Xu/cEwPtUbZK7QvWO0NCwurJU8Htkrc31Bxvytq53IfPUlZxSBOlCBQKi9UzzTUyOK6G4SlhSWSUrZAEjvIBs3pkBB3wiGQbhMFz23vH3Tz89hZebo9FWedZOUDc756dSavmvd523QMDJ18iHD/2ReMb05zd9wRriHJo402VXpNH37rHwEg96Vi2qplfiA7wtGtUlWyaviC/Ag2VdjlHbBRPEZ1YoOEzd0d4GB0tZESQ3qOgAdFllu+XY9sbN9zMzlMc6nW2wbiwR/sXD60YNdXUDWcZ12tSEdWd4pb6IpukglEl2XJ2SB8lkvalo4V5q32lISst64Jv1oA7SBUzx7NUwlK1/A0FSpbdUYYMQWmssD288+YxMs687EWxRU9xtW2L6gaV/VmavTUnT+r5xsicL7dY7XWhgyOfRvKhli5LXeRynq6xbg/Jo87N9eZJ4fCmmeFgsjhUVy3/ej9H9VKdBKLAJWudK1PoCtNyOS2riWd0QfLvaxyZQ43xJpKxkzilvxBAaTwNg14Zs06OqPxxY0YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":983,"cache_read_input_tokens":106770,"cache_creation":{"ephemeral_5m_input_tokens":983,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1bcd618b-0ee3-4aa3-9e13-3e10bcb61a50"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLEbePp7YbdBKZt5hjH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016mFiYuo71kc4kHViNyXD7m","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nfrom louka import serpapi_client\nd = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'en'})\nprint('hl=en — n:', len(d.get('reviews') or []), '| pagination:', (d.get('serpapi_pagination') or {}).get('next_page_token', None) is not None)\nd2 = serpapi_client.search({'engine':'google_maps_reviews','data_id':'0x4cb893d54cddc42b:0xef36e52580285ad9','hl':'fr','sort_by':'newestFirst','no_cache':'true'})\nprint('no_cache — n:', len(d2.get('reviews') or []), '| pagination:', d2.get('serpapi_pagination'))\n\" 2>&1 | grep -v cede959","description":"Test hl=en and no_cache pagination","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":983,"cache_read_input_tokens":106770,"cache_creation":{"ephemeral_5m_input_tokens":983,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2a6e1d5d-f6ca-4d18-a827-f6e38212ab1d"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bwncyk7ak","tool_use_id":"toolu_016mFiYuo71kc4kHViNyXD7m","description":"Test hl=en and no_cache pagination","task_type":"local_bash","uuid":"131eee7d-1182-4b6e-b5e3-fe4e394d48f9","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bwncyk7ak","tool_use_id":"toolu_016mFiYuo71kc4kHViNyXD7m","status":"completed","output_file":"","summary":"Test hl=en and no_cache pagination","uuid":"85a3aa01-099d-42ca-b3f2-5e0a0467e64a","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016mFiYuo71kc4kHViNyXD7m","type":"tool_result","content":"hl=en — n: 8 | pagination: False\nno_cache — n: 8 | pagination: None","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4b1256d0-4985-48b9-a494-e7679b4946a4","timestamp":"2026-08-24T22:05:56.379Z","tool_use_result":{"stdout":"hl=en — n: 8 | pagination: False\nno_cache — n: 8 | pagination: None","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"ee9fdc55-24d2-4230-80a5-dadd7f2d0d29","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLFo3H6cHirrPtwrmHb","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS/AgKpwEIERgCKkDFc2AjLaQ6HjuDTdCEg7RXcItMT5VOKemyzDEKD0PBOGUfFOzzFSb6nXTcVhXoGqLKZRBxSsyY7Qy5aPSWiVqBMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHQgLPUBrABAhIMND/WS5xrGc1L998iGgzsgQir7xOkUq0zOnwiMEuxok9/pmEZjcY12r66I14u5fDV3vbz5zljsrUG9BRK4MKqmKJm713nfc0fjpkt4yqBB30qi/16MO1dwL03j+GzPjS+nHRtHY7nW5AAQQ7w69GCtNBChZiTyhc+KiaR5oYuYJULnzes8MNmbEj12O7xmeEdydZqCBsdOQEkD1/SKnifnP9ylIVyXIVw4NgjmxNZ9HQnQ6cdU3hErdOCACkMdTeTkGjUjC+RCAf++YUUK9s4ZSpVOzdhIdYNR9NOtcULpSpvg1du4E10t2TQ3AQO+iqaQnyD76ZpEuK+1TmymAxHkzhJbAe9ghETmHazsMRDK2IhkpeKcUNBjlNRJp7vaZ+IndLSlk+cqRDi9Udo5AnWUZHBj+vZAsV+83Pgem9x8f7XC8ouVd9mjHCbbyu9vop3IBxHdyyO9txcIHCuu2Q3NfZqJdTE9EJD1z4rhzUpSq1hOopdO+Mg5NEYpg5ykmOT/Ybo0U1GObRIEsWy34Yn5cujEfhm/4F1HNxR/Zw/VSO+9taNHrlP5IHb9MjPOdExh1kNxn+8p4VPvgYk/6LbMc731QB5T3p7gTMcj7Wv5MxNg5MMbUOcNnv5MikmdXu6awQOF4hXjH6wkYNAR3Nd51q87txK8y+ixRbf23K/ylSUIci2PUUkGV8kZe7rusi02LHLzDUtqInToWg8JaqM/5G9aGggQ/ayUcAXYi/gBYhUsIgALbqpraIvN21gI8057+TuNROpwYHm2RTHK71MtAt+Nv3t54Ajb+s+nnygunzRZ9doAeHUL04+3M51BOTnPdCIll9Sl77PlWHOIfcoLoWWbQr0Im8Rcb5pXz9KFv6BWG4CF/jgDr8sRhVsfgpDf8251lL8a+fXzviaU7a2oTnfBY0hJr8IrHqgklb6Pf0mYGO9PY25IUF9NmyY349PD3H1H3HqQvZjuYNUSZFiuLFqZg3Lcn+KaMM+bPN5qclDR/G+kprG6kCzz2P+5c4sGuSuUrUNQ1rhDV7P4C3XgZSh9jmh35jHNry39ElfQDE4Mfx6U3dnXNWTDXl9J7VojNF/+k5b1yAEMrdPA6+qd/z+7NAEKypLJtIqSouf8p/JC8gAF66+7VOG0NkeltojnysucC7LMIL4iQnzJi5hk9NTGuQ3ALZtawnUC/UmLizxENPk/muIfEGCrCNWCywiGzeNwQMfblaDw8fkckCOuxpHP/Om5MyPEjkMit43DOzlZpOWELootlZxL517rREOZkH0/tI18BwBkNYeWfV8MxgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":688,"cache_read_input_tokens":107753,"cache_creation":{"ephemeral_5m_input_tokens":688,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0c53cf01-562c-4ac4-8089-2bb525fd9137"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLFo3H6cHirrPtwrmHb","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS0AUKqAEIERgCKkBAm73Yn0y6R2K+zcz+P/iOc2LvPd2EnFGKpRCDl4K6hBkcIKAYRPvbc4eG5tvvkfZUS77bmtARbMPApGBHqxAWMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagB0ICz1AawAQISDNP7PqgksK9Q1cnftRoMnRK8rxxMa7fMpvPfIjBcDuoWZs5AX8SYImVgHzjnFsyvC10xh3nu/06lhozOFsgP7B3iJT25OZv9l4gcdKEq1AP1ea7ytJ6cUg2G8bNzISxfTSoA5qcnq5zR3bxVeEIvLlfchvazG1yo965Dk9K/P033harf4lkWadrGwYYQ8QSwTRyDZTJOOckW2yI+1fmLELfqLLTHfImhYVxUdX3iID3pVv1Ql6d3ApsI2hnSEc22qaexLGoKBiXnuU9Cx0NRwtuxqa6lQHCL+wBF/AeUgGQeZOWE6PNrCeiFkvRcyjvynJ6MRlvh0oRIysynb1m9AJyXUkmv+voNOZoGc0zXf+ZhycNnagql4/7thBJc5QlrQBWS8NIo0VhEHF7XLoBC3iCkORDjw/RKBi7C8dBuSk/3O2QcGslkT09ThJY65C2YqucP8BhJAqS1A9I/g88kHeYwrMkY6MHy3kODr8rGUqZaNAsk4wpNbctBfGgjZm2EL91LQJbA6n0xisnSee+QaBVidRe4RXYKjFUGzGZKyo7GPwt2pfVTgNgKVyyoXrA5XKbKm14niG7AZDdd4nCcJeZU0hapwmoqhiYpi1Ue6YmQuWIgz3XtuUBB2nZ/QVixaNQ5nXlpEPw27TREAW5sFcJprqGj2ygmc8bbt79NnXoWFzFm6sTS8MEr8N+O936kh41KNF/W/ml+MvDXaxcUyOorfQEYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":688,"cache_read_input_tokens":107753,"cache_creation":{"ephemeral_5m_input_tokens":688,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d04b7045-f468-4e8b-862b-05b69ec8d7c9"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLFo3H6cHirrPtwrmHb","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QendUMRmB4x4pHgKWW26nQ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py","old_string":" p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n p[\"next_page_token\"] = next_page_token\n return search(p)","new_string":" p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n # num n'est accepté par SerpApi qu'avec un next_page_token\n p[\"next_page_token\"] = next_page_token\n p[\"num\"] = 20\n return search(p)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":688,"cache_read_input_tokens":107753,"cache_creation":{"ephemeral_5m_input_tokens":688,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fe50515e-3012-4081-99d4-a3ac8098b587"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QendUMRmB4x4pHgKWW26nQ","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4e8216d3-a872-4db9-b54c-f3a6f4b0efb4","timestamp":"2026-08-24T22:06:10.982Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/serpapi_client.py","oldString":" p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n p[\"next_page_token\"] = next_page_token\n return search(p)","newString":" p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n # num n'est accepté par SerpApi qu'avec un next_page_token\n p[\"next_page_token\"] = next_page_token\n p[\"num\"] = 20\n return search(p)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# serpapi_client.py : client SerpApi minimal, STRICTEMENT côté serveur.\n#\n# Sécurité de la clé (non négociable) :\n# - lue UNIQUEMENT depuis la variable d'environnement SERPAPI_API_KEY\n# (chargée par run.py depuis .env, jamais commitée — voir .env.example) ;\n# - jamais écrite dans les logs, les exceptions ni les réponses API ;\n# - aucune requête SerpApi ne part du navigateur : ce module n'est importé\n# que par les jobs serveur (managers.py).\n#\n# Robustesse : timeout 30 s, 3 tentatives avec repli exponentiel sur les\n# erreurs transitoires (réseau, 429, 5xx). Les erreurs définitives (4xx)\n# remontent immédiatement.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport os\nimport time\n\nimport requests\n\nBASE = \"https://serpapi.com/search.json\"\nTIMEOUT = 30\nRETRIES = 3\nBACKOFF = 2.0 # s — doublé à chaque tentative\n\n\nclass SerpApiError(RuntimeError):\n pass\n\n\ndef api_key() -> str | None:\n return os.environ.get(\"SERPAPI_API_KEY\") or None\n\n\ndef available() -> bool:\n return api_key() is not None\n\n\ndef search(params: dict) -> dict:\n \"\"\"GET serpapi.com/search.json — params SANS la clé (ajoutée ici).\"\"\"\n key = api_key()\n if not key:\n raise SerpApiError(\"SERPAPI_API_KEY absente de l'environnement\")\n q = dict(params)\n q[\"api_key\"] = key\n last = \"erreur inconnue\"\n for attempt in range(RETRIES):\n if attempt:\n time.sleep(BACKOFF * (2 ** (attempt - 1)))\n try:\n r = requests.get(BASE, params=q, timeout=TIMEOUT)\n except requests.RequestException as e:\n # ne jamais inclure l'URL complète (elle contient la clé)\n last = f\"erreur réseau: {type(e).__name__}\"\n continue\n if r.status_code == 429 or r.status_code >= 500:\n last = f\"HTTP {r.status_code}\"\n continue\n if r.status_code != 200:\n raise SerpApiError(f\"SerpApi HTTP {r.status_code}\")\n data = r.json()\n if data.get(\"error\"):\n # « fully empty » = zéro résultat, pas une panne\n if \"hasn't returned any results\" in str(data[\"error\"]):\n return data\n raise SerpApiError(f\"SerpApi: {data['error']}\")\n return data\n raise SerpApiError(f\"SerpApi injoignable après {RETRIES} tentatives ({last})\")\n\n\ndef google_maps(query: str) -> dict:\n return search({\"engine\": \"google_maps\", \"q\": query, \"hl\": \"fr\",\n \"gl\": \"ca\", \"google_domain\": \"google.ca\", \"type\": \"search\"})\n\n\ndef google_maps_reviews(data_id: str, next_page_token: str | None = None) -> dict:\n p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,\n \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}\n if next_page_token:\n p[\"next_page_token\"] = next_page_token\n return search(p)\n","structuredPatch":[{"oldStart":80,"oldLines":5,"newStart":80,"newLines":7,"lines":[" p = {\"engine\": \"google_maps_reviews\", \"data_id\": data_id,"," \"hl\": \"fr\", \"sort_by\": \"newestFirst\"}"," if next_page_token:","+ # num n'est accepté par SerpApi qu'avec un next_page_token"," p[\"next_page_token\"] = next_page_token","+ p[\"num\"] = 20"," return search(p)"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"7e5ded66-65fb-4e16-84d6-a7aaadeacfcf","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLGsesUZQdfxjYTyedk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01L5CjZUej4afYyyt1KmXYEv","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","old_string":" rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}","new_string":" rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) — l'échantillon peut être \"\n \"plus petit que le total annoncé par Google, il \"\n \"s'accumule à chaque synchronisation ; thèmes \"\n \"par lexique — inférence, pas une lecture \"\n \"humaine\")}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1454,"cache_read_input_tokens":108441,"cache_creation":{"ephemeral_5m_input_tokens":1454,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3a224b95-c3fb-4ddd-a02e-119f50cd399e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01L5CjZUej4afYyyt1KmXYEv","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/managers.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c8ac0676-1040-4c11-a95b-708608c582e5","timestamp":"2026-08-24T22:06:21.610Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/managers.py","oldString":" rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}","newString":" rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) — l'échantillon peut être \"\n \"plus petit que le total annoncé par Google, il \"\n \"s'accumule à chaque synchronisation ; thèmes \"\n \"par lexique — inférence, pas une lecture \"\n \"humaine\")}","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# managers.py : « Qui gère ce logement? » — fiche du gestionnaire/propriétaire.\n#\n# 1) Amorçage : les gestionnaires sont les sources DIRECTES de Lou-Ka\n# (data/sources.json, hors portails et petites annonces) qui ont au moins\n# une annonce collectée. Aucun gestionnaire n'est inventé.\n# 2) Résolution Google Maps (SerpApi, serveur seulement) : la fiche Google\n# n'est associée QUE si la confiance est suffisante (similarité de nom,\n# domaine du site web, ville) — jamais « le premier résultat » aveuglément.\n# Signaux et confiance stockés (match_confidence, match_signals).\n# 3) Avis Google : synchronisation paginée (next_page_token, hl=fr, plus\n# récents d'abord), dédupliqués par (manager_id, review_id Google) avec\n# repli sur un hash de contenu. Les avis vivent dans NOTRE base : les pages\n# ne déclenchent jamais d'appel SerpApi.\n# 4) Analyse : statistiques réelles (distribution, récence, tendance — pas\n# seulement la moyenne Google) + thèmes par lexique français, sentiment\n# dérivé de la note, sévérité et confiance. Statut « inferred » assumé.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport difflib\nimport hashlib\nimport json\nimport re\nimport time\nimport unicodedata\nfrom datetime import datetime, timezone\n\nfrom . import db, serpapi_client\nfrom .dedup import CLASSIFIEDS, PORTALS\n\nVERSION = \"gestionnaire-1.0\"\nSEUIL_CONFIANCE = 0.60 # sous ce seuil : fiche Google NON associée\nMAX_PAGES = 10 # garde-fou pagination avis (par synchronisation)\nSYNC_TTL = 7 * 86400 # re-synchroniser les avis au plus aux 7 jours\nRESOLVE_RETRY = 30 * 86400 # re-tenter une résolution échouée après 30 jours\n\n# suffixes/génériques retirés pour comparer les noms d'entreprises\n_LEGAL = re.compile(\n r\"\\b(inc|ltee|ltée|ltd|senc|s\\.e\\.n\\.c|enr|corp|corporation|groupe|group|\"\n r\"gestion|gestions|immobilier|immobiliere|immobilière|immeubles?|\"\n r\"appartements?|apartments?|properties|proprietes|propriétés|residences?|\"\n r\"résidences?|habitations?|siege|social|bureau|head|office|\"\n r\"les|le|la|de|du|des|et|and)\\b\")\n\n# types Google d'une fiche d'ENTREPRISE (vs fiche d'un immeuble précis)\n_CORP_TYPES = re.compile(\n r\"agence immobili|gestion immobili|property management|real estate|\"\n r\"societe de gestion|immobilier\", re.I)\n\n\ndef _norm_name(s: str | None) -> str:\n s = unicodedata.normalize(\"NFKD\", (s or \"\").lower())\n s = \"\".join(c for c in s if not unicodedata.combining(c))\n s = re.sub(r\"[^a-z0-9 ]+\", \" \", s)\n s = _LEGAL.sub(\" \", s)\n return \" \".join(s.split())\n\n\ndef _domain(url: str | None) -> str | None:\n if not url:\n return None\n m = re.search(r\"^(?:https?://)?(?:www\\.)?([^/:?#]+)\", url.strip().lower())\n return m.group(1) if m else None\n\n\n# --- 1) amorçage depuis sources.json ----------------------------------------\n\ndef seed(con=None) -> dict:\n \"\"\"Crée/actualise un gestionnaire par source directe ayant des annonces.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n sources = json.loads((db.DB_PATH.parent / \"sources.json\")\n .read_text(\"utf-8\"))[\"sources\"]\n with_ads = {r[\"source\"] for r in con.execute(\n \"SELECT DISTINCT source FROM listings\")}\n n = 0\n for s in sources:\n sid = s.get(\"id\")\n if not sid or sid in PORTALS or sid in CLASSIFIEDS:\n continue\n if sid not in with_ads and s.get(\"connector\") not in with_ads:\n continue\n con.execute(\n \"INSERT INTO managers (source_id, name, website, aliases)\"\n \" VALUES (?,?,?,?)\"\n \" ON CONFLICT(source_id) DO UPDATE SET name=excluded.name,\"\n \" website=excluded.website\",\n (sid, s.get(\"name\"), s.get(\"url\"),\n json.dumps({\"region\": s.get(\"region\"),\n \"connector\": s.get(\"connector\")},\n ensure_ascii=False)))\n n += 1\n con.commit()\n return {\"managers\": n}\n finally:\n if own:\n con.close()\n\n\n# --- 2) résolution Google Maps (jamais « le premier résultat ») --------------\n\ndef _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\n signals: dict = {}\n sim = difflib.SequenceMatcher(\n None, _norm_name(mgr[\"name\"]), _norm_name(cand.get(\"title\"))).ratio()\n signals[\"similarite_nom\"] = round(sim, 3)\n conf = 0.6 * sim\n d_mgr, d_cand = _domain(mgr.get(\"website\")), _domain(cand.get(\"website\"))\n if d_mgr and d_cand and d_mgr == d_cand:\n conf += 0.3\n signals[\"meme_domaine_web\"] = d_mgr\n region = None\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\"region\")\n except (ValueError, TypeError):\n pass\n addr = (cand.get(\"address\") or \"\").lower()\n if region and _norm_name(region) and _norm_name(region) in _norm_name(addr):\n conf += 0.1\n signals[\"ville_correspondante\"] = region\n if _CORP_TYPES.search(cand.get(\"type\") or \"\"):\n signals[\"fiche_entreprise\"] = cand.get(\"type\")\n signals[\"type_fiche\"] = cand.get(\"type\")\n signals[\"nombre_avis\"] = cand.get(\"reviews\")\n return min(1.0, conf), signals\n\n\ndef resolve(manager_id: int, con=None) -> dict:\n \"\"\"Associe (ou refuse d'associer) la fiche Google Maps du gestionnaire.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None:\n return {\"ok\": False, \"raison\": \"gestionnaire inconnu\"}\n mgr = dict(mgr)\n region = \"\"\n try:\n region = (json.loads(mgr.get(\"aliases\") or \"{}\") or {}).get(\n \"region\") or \"\"\n except (ValueError, TypeError):\n pass\n q = f\"{mgr['name']} {region} Québec\".strip()\n data = serpapi_client.google_maps(q)\n cands = data.get(\"local_results\") or []\n if not cands and data.get(\"place_results\"):\n cands = [data[\"place_results\"]]\n\n scored = [( *_score_candidate(mgr, c), c) for c in cands[:8]]\n best, best_conf, best_sig = None, 0.0, {}\n if scored:\n top = max(s[0] for s in scored)\n # une chaîne a souvent une fiche Google PAR IMMEUBLE en plus du\n # siège : parmi les candidats plausibles (confiance proche du\n # meilleur ET au-dessus du seuil), préférer la fiche d'entreprise\n # puis la plus représentative (le plus d'avis)\n near = [s for s in scored\n if s[0] >= max(SEUIL_CONFIANCE, top - 0.15)]\n if near:\n best_conf, best_sig, best = max(\n near, key=lambda s: (bool(_CORP_TYPES.search(\n s[2].get(\"type\") or \"\")),\n s[2].get(\"reviews\") or 0, s[0]))\n else:\n best_conf, best_sig, best = max(scored, key=lambda s: s[0])\n now = time.time()\n if best is None or best_conf < SEUIL_CONFIANCE:\n con.execute(\n \"UPDATE managers SET resolve_failed=1, resolved_at=?,\"\n \" match_confidence=?, match_signals=? WHERE id=?\",\n (now, round(best_conf, 3) if best else None,\n json.dumps({\"refus\": \"confiance insuffisante\",\n \"candidats\": len(cands), **best_sig},\n ensure_ascii=False), manager_id))\n con.commit()\n return {\"ok\": False, \"raison\": \"confiance insuffisante\",\n \"confiance\": round(best_conf, 3)}\n con.execute(\n \"UPDATE managers SET gmaps_place_id=?, gmaps_data_id=?,\"\n \" gmaps_name=?, gmaps_address=?, gmaps_rating=?, gmaps_reviews=?,\"\n \" match_confidence=?, match_signals=?, resolved_at=?,\"\n \" resolve_failed=0, phone=COALESCE(phone, ?) WHERE id=?\",\n (best.get(\"place_id\"), best.get(\"data_id\"), best.get(\"title\"),\n best.get(\"address\"), best.get(\"rating\"), best.get(\"reviews\"),\n round(best_conf, 3), json.dumps(best_sig, ensure_ascii=False),\n now, best.get(\"phone\"), manager_id))\n con.commit()\n return {\"ok\": True, \"gmaps_name\": best.get(\"title\"),\n \"confiance\": round(best_conf, 3)}\n finally:\n if own:\n con.close()\n\n\n# --- 3) synchronisation des avis (paginée, dédupliquée, en base) -------------\n\ndef _content_hash(author: str | None, rating, text: str | None) -> str:\n raw = f\"{author or ''}|{rating}|{(text or '')[:200]}\"\n return hashlib.sha1(raw.encode(\"utf-8\")).hexdigest()\n\n\ndef sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\n \"\"\"Rapatrie les avis Google (les plus récents d'abord) dans NOTRE base.\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE id=?\",\n (manager_id,)).fetchone()\n if mgr is None or not mgr[\"gmaps_data_id\"]:\n return {\"ok\": False, \"raison\": \"fiche Google non associée\"}\n known = {r[\"external_review_id\"] for r in con.execute(\n \"SELECT external_review_id FROM manager_reviews WHERE manager_id=?\",\n (manager_id,))}\n known_hash = {r[\"content_hash\"] for r in con.execute(\n \"SELECT content_hash FROM manager_reviews WHERE manager_id=?\"\n \" AND external_review_id IS NULL\", (manager_id,))}\n\n token, new, pages = None, 0, 0\n while pages < max_pages:\n data = serpapi_client.google_maps_reviews(\n mgr[\"gmaps_data_id\"], next_page_token=token)\n reviews = data.get(\"reviews\") or []\n pages += 1\n page_new = 0\n now = time.time()\n for rv in reviews:\n ext = rv.get(\"review_id\")\n text = rv.get(\"snippet\") or rv.get(\"extracted_snippet\",\n {}).get(\"original\")\n user = rv.get(\"user\") or {}\n chash = _content_hash(user.get(\"name\"), rv.get(\"rating\"), text)\n if (ext and ext in known) or (not ext and chash in known_hash):\n continue\n resp = rv.get(\"response\") or {}\n analysis = _analyze_review(rv.get(\"rating\"), text)\n con.execute(\n \"INSERT INTO manager_reviews (manager_id,\"\n \" external_review_id, source, rating, text, published_at,\"\n \" relative_date_raw, author_name, author_review_count,\"\n \" owner_response, owner_response_date, source_url,\"\n \" fetched_at, content_hash, analysis) VALUES\"\n \" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)\"\n \" ON CONFLICT(manager_id, external_review_id) DO NOTHING\",\n (manager_id, ext, \"google_maps\", rv.get(\"rating\"), text,\n rv.get(\"iso_date\"), rv.get(\"date\"), user.get(\"name\"),\n user.get(\"reviews\"), resp.get(\"snippet\"),\n resp.get(\"iso_date\") or resp.get(\"date\"), rv.get(\"link\"),\n now, chash, json.dumps(analysis, ensure_ascii=False)))\n if ext:\n known.add(ext)\n else:\n known_hash.add(chash)\n page_new += 1\n new += page_new\n token = (data.get(\"serpapi_pagination\") or {}).get(\n \"next_page_token\")\n # tri « plus récents d'abord » : une page entièrement connue\n # signifie qu'on a rejoint l'historique déjà synchronisé\n if not token or (reviews and page_new == 0):\n break\n stats = _aggregate(con, manager_id)\n con.execute(\"UPDATE managers SET review_stats=?, last_synced_at=?\"\n \" WHERE id=?\",\n (json.dumps(stats, ensure_ascii=False), time.time(),\n manager_id))\n con.commit()\n return {\"ok\": True, \"nouveaux\": new, \"pages\": pages,\n \"total\": stats.get(\"n\")}\n finally:\n if own:\n con.close()\n\n\n# --- 4) analyse : thèmes (lexique fr), sentiment, sévérité, agrégats ---------\n\nTOPICS = {\n \"entretien\": [\"entretien\", \"entreten\", \"maintenance\", \"neglige\", \"négligé\",\n \"delabre\", \"délabré\", \"moisissure\"],\n \"réparations\": [\"reparation\", \"réparation\", \"reparer\", \"réparer\", \"bris\",\n \"brise\", \"brisé\", \"fuite\", \"degat\", \"dégât\", \"plomberie\"],\n \"communication\": [\"communication\", \"repond\", \"répond\", \"reponse\",\n \"réponse\", \"rappel\", \"courriel\", \"joindre\",\n \"injoignable\", \"appel\", \"message\"],\n \"personnel\": [\"personnel\", \"employe\", \"employé\", \"concierge\", \"equipe\",\n \"équipe\", \"gerant\", \"gérant\", \"agent\", \"proprietaire\",\n \"propriétaire\"],\n \"propreté\": [\"propre\", \"proprete\", \"propreté\", \"sale\", \"salete\", \"saleté\",\n \"malpropre\"],\n \"chauffage\": [\"chauffage\", \"chauffe\", \"chauffé\", \"froid\", \"temperature\",\n \"température\"],\n \"eau chaude\": [\"eau chaude\"],\n \"vermine\": [\"punaise\", \"coquerelle\", \"souris\", \"rat \", \"rats\", \"insecte\",\n \"vermine\", \"fourmi\", \"extermina\"],\n \"bruit\": [\"bruit\", \"bruyant\", \"tapage\", \"insonoris\"],\n \"sécurité\": [\"securite\", \"sécurité\", \"securitaire\", \"sécuritaire\", \"vol\",\n \"intrusion\", \"serrure\", \"camera\", \"caméra\"],\n \"ascenseur\": [\"ascenseur\"],\n \"stationnement\": [\"stationnement\", \"parking\", \"deneigement\",\n \"déneigement\"],\n \"dépôt et remboursement\": [\"depot\", \"dépôt\", \"rembours\", \"caution\"],\n \"augmentation de loyer\": [\"augmentation\", \"hausse\"],\n \"bail et location\": [\"bail\", \"signature\", \"visite\", \"location\"],\n \"service client\": [\"service\", \"clientele\", \"clientèle\", \"professionnel\",\n \"courtois\", \"arrogant\", \"impoli\", \"respect\"],\n}\n_GRAVES = {\"vermine\", \"sécurité\", \"chauffage\", \"eau chaude\"}\n\n\ndef _analyze_review(rating, text: str | None) -> dict:\n t = (text or \"\").lower()\n topics = sorted({name for name, kws in TOPICS.items()\n if any(k in t for k in kws)})\n if rating is None:\n sentiment = \"inconnu\"\n elif rating <= 2:\n sentiment = \"négatif\"\n elif rating >= 4:\n sentiment = \"positif\"\n else:\n sentiment = \"neutre\"\n severity = \"faible\"\n if sentiment == \"négatif\":\n severity = \"élevée\" if any(x in _GRAVES for x in topics) else \"moyenne\"\n if len(t) >= 80 and topics:\n confidence = 0.7\n elif t:\n confidence = 0.4\n else:\n confidence = 0.2\n return {\"topics\": topics, \"sentiment\": sentiment, \"severite\": severity,\n \"confiance\": confidence, \"statut\": \"inferred\",\n \"methode\": \"lexique de thèmes fr + sentiment dérivé de la note\"}\n\n\ndef _aggregate(con, manager_id: int) -> dict:\n rows = con.execute(\n \"SELECT rating, published_at, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\", (manager_id,)).fetchall()\n rated = [r for r in rows if r[\"rating\"] is not None]\n out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\",\n \"methode\": (\"statistiques calculées sur les avis Google \"\n \"synchronisés en base (pas seulement la moyenne \"\n \"affichée par Google) ; thèmes par lexique — \"\n \"inférence, pas une lecture humaine\")}\n if not rated:\n return out\n out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)\n dist = {str(i): 0 for i in range(1, 6)}\n for r in rated:\n k = str(int(round(r[\"rating\"])))\n if k in dist:\n dist[k] += 1\n out[\"distribution\"] = dist\n out[\"pct_negatif\"] = round(100 * (dist[\"1\"] + dist[\"2\"]) / len(rated))\n out[\"pct_positif\"] = round(100 * (dist[\"4\"] + dist[\"5\"]) / len(rated))\n out[\"avec_reponse_proprietaire\"] = sum(\n 1 for r in rows if r[\"owner_response\"])\n\n cutoff = datetime.now(timezone.utc).timestamp() - 365 * 86400\n rec, old = [], []\n for r in rated:\n ts = _iso_ts(r[\"published_at\"])\n (rec if ts and ts >= cutoff else old).append(r[\"rating\"])\n if rec:\n out[\"moyenne_12m\"] = round(sum(rec) / len(rec), 2)\n out[\"n_12m\"] = len(rec)\n if len(rec) >= 5 and len(old) >= 5:\n delta = out[\"moyenne_12m\"] - sum(old) / len(old)\n out[\"tendance\"] = (\"en amélioration\" if delta >= 0.3 else\n \"en dégradation\" if delta <= -0.3 else \"stable\")\n out[\"tendance_delta\"] = round(delta, 2)\n\n themes: dict[str, dict] = {}\n for r in rows:\n try:\n a = json.loads(r[\"analysis\"] or \"{}\")\n except (ValueError, TypeError):\n continue\n for tp in a.get(\"topics\") or []:\n d = themes.setdefault(tp, {\"mentions\": 0, \"negatif\": 0,\n \"positif\": 0})\n d[\"mentions\"] += 1\n if a.get(\"sentiment\") == \"négatif\":\n d[\"negatif\"] += 1\n elif a.get(\"sentiment\") == \"positif\":\n d[\"positif\"] += 1\n out[\"themes\"] = dict(sorted(themes.items(),\n key=lambda kv: -kv[1][\"mentions\"]))\n out[\"plaintes_frequentes\"] = [\n k for k, v in sorted(themes.items(), key=lambda kv: -kv[1][\"negatif\"])\n if v[\"negatif\"] >= 2][:5]\n return out\n\n\ndef _iso_ts(iso: str | None) -> float | None:\n if not iso:\n return None\n try:\n return datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\")).timestamp()\n except ValueError:\n return None\n\n\n# --- fiche API + boucle de fond ----------------------------------------------\n\ndef fiche(source_id: str, con=None) -> dict | None:\n \"\"\"Fiche gestionnaire pour l'API — tout vient de NOTRE base (0 SerpApi).\"\"\"\n own = con is None\n if own:\n con = db.connect()\n try:\n mgr = con.execute(\"SELECT * FROM managers WHERE source_id=?\",\n (source_id,)).fetchone()\n if mgr is None:\n return None\n m = dict(mgr)\n n_active = con.execute(\n \"SELECT COUNT(*) n FROM listings WHERE source=? AND active=1\"\n \" AND published=1 AND dup_of IS NULL\", (source_id,)).fetchone()[\"n\"]\n out = {\n \"source_id\": m[\"source_id\"], \"nom\": m[\"name\"],\n \"site_web\": m[\"website\"], \"telephone\": m[\"phone\"],\n \"annonces_actives\": n_active, \"version\": VERSION,\n }\n if m[\"gmaps_place_id\"] and not m[\"resolve_failed\"]:\n out[\"google_maps\"] = {\n \"nom\": m[\"gmaps_name\"], \"adresse\": m[\"gmaps_address\"],\n \"note\": m[\"gmaps_rating\"], \"nombre_avis\": m[\"gmaps_reviews\"],\n \"place_id\": m[\"gmaps_place_id\"],\n \"confiance_association\": m[\"match_confidence\"],\n \"signaux\": json.loads(m[\"match_signals\"] or \"{}\"),\n \"statut\": \"inferred\",\n \"methode\": (\"fiche associée par similarité de nom, domaine \"\n \"web et ville — association refusée sous \"\n f\"{SEUIL_CONFIANCE:.2f} de confiance\"),\n }\n try:\n out[\"avis\"] = json.loads(m[\"review_stats\"] or \"null\")\n except (ValueError, TypeError):\n out[\"avis\"] = None\n out[\"avis_recents\"] = [\n {\"note\": r[\"rating\"], \"texte\": r[\"text\"],\n \"date\": r[\"published_at\"] or r[\"relative_date_raw\"],\n \"auteur\": r[\"author_name\"],\n \"reponse_proprietaire\": bool(r[\"owner_response\"]),\n \"analyse\": json.loads(r[\"analysis\"] or \"{}\"),\n \"statut\": \"observed\"}\n for r in con.execute(\n \"SELECT rating, text, published_at, relative_date_raw,\"\n \" author_name, owner_response, analysis\"\n \" FROM manager_reviews WHERE manager_id=?\"\n \" ORDER BY published_at DESC, fetched_at DESC LIMIT 10\",\n (m[\"id\"],))]\n out[\"derniere_synchro\"] = m[\"last_synced_at\"]\n elif m[\"resolved_at\"]:\n out[\"google_maps\"] = {\n \"statut\": \"non_associe\",\n \"note_methode\": (\"aucune fiche Google Maps n'a pu être \"\n \"associée avec une confiance suffisante — \"\n \"Lou-Ka préfère ne rien afficher plutôt que \"\n \"d'attribuer les avis d'une autre entreprise\"),\n }\n return out\n finally:\n if own:\n con.close()\n\n\ndef precompute(budget_s: float = 120.0, con=None) -> dict:\n \"\"\"Boucle de fond : amorce, résout et synchronise dans un budget temps.\"\"\"\n if not serpapi_client.available():\n return {\"skipped\": \"SERPAPI_API_KEY absente\"}\n own = con is None\n if own:\n con = db.connect()\n t0 = time.time()\n resolved = synced = errors = 0\n try:\n seed(con)\n now = time.time()\n # 1) résolutions manquantes (ou échecs anciens à retenter)\n for r in con.execute(\n \"SELECT id FROM managers WHERE resolved_at IS NULL\"\n \" OR (resolve_failed=1 AND resolved_at < ?)\"\n \" ORDER BY resolved_at IS NOT NULL, id\",\n (now - RESOLVE_RETRY,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if resolve(r[\"id\"], con).get(\"ok\"):\n resolved += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break # panne API : ne pas insister ce tour-ci\n # 2) avis périmés (les jamais-synchronisés d'abord)\n for r in con.execute(\n \"SELECT id FROM managers WHERE gmaps_data_id IS NOT NULL\"\n \" AND resolve_failed=0 AND (last_synced_at IS NULL\"\n \" OR last_synced_at < ?)\"\n \" ORDER BY last_synced_at IS NOT NULL, last_synced_at\",\n (now - SYNC_TTL,)).fetchall():\n if time.time() - t0 > budget_s:\n break\n try:\n if sync_reviews(r[\"id\"], con).get(\"ok\"):\n synced += 1\n except serpapi_client.SerpApiError:\n errors += 1\n break\n return {\"resolved\": resolved, \"synced\": synced, \"errors\": errors,\n \"elapsed_s\": round(time.time() - t0, 1)}\n finally:\n if own:\n con.close()\n\n\nif __name__ == \"__main__\":\n print(precompute())\n","structuredPatch":[{"oldStart":348,"oldLines":8,"newStart":348,"newLines":11,"lines":[" out: dict = {\"n\": len(rows), \"version\": VERSION, \"statut\": \"calculated\","," \"methode\": (\"statistiques calculées sur les avis Google \""," \"synchronisés en base (pas seulement la moyenne \"","- \"affichée par Google) ; thèmes par lexique — \"","- \"inférence, pas une lecture humaine\")}","+ \"affichée par Google) — l'échantillon peut être \"","+ \"plus petit que le total annoncé par Google, il \"","+ \"s'accumule à chaque synchronisation ; thèmes \"","+ \"par lexique — inférence, pas une lecture \"","+ \"humaine\")}"," if not rated:"," return out"," out[\"moyenne\"] = round(sum(r[\"rating\"] for r in rated) / len(rated), 2)"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"32f7cc18-2480-4e93-8794-75ad22f913db","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLHepUidQGiJWL5Ma2p","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS4gQKpwEIERgCKkDgUIiSNoTE1vKxURhuIjS1ft3lqNGzKfodh38yxkTm5yC7V+CnXg0zsWoeCp8+tBq345fsyd5dsZMEtTdnmR+jMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHjgLPUBrABAhIMh/fShFblinafkDm0GgycCFHIEb9Ap0DbPIgiMNSG9wn73SQxtrMjw828V0bKd4OLJhISCN5Tf4UnttfmCVPyf532IyfjoLJyGESu8CrnAopWQEEEV9dpx5FxMDOBcsFb/4qG+xwbMLkHlal62gf3Hn4hdKRZxgikJ07oIo+1MGvwFvsCOPwv2uTyFz/5vc3DcQpJqMTzWR7h9JLUi9BqiUowFzmjuzIxFNA6RKgjsLbHUl0rLoLriAr8PKOX8jdAHOqbc/2ZuK0OAptHXLdNK0WPEQMXk9WhIBhnEPLELuPAbIt8jVUwVCredr3s1WoYpu5Tb+doQfjvt2lSMSEFWhDvkN5Wb72MQDq7ASAeRTJThPK3aU2tGA4X8D2wKikLXy5gyVhlXrN61Les/ounWDpDiGLig5+xwUcBlI9vjQHi3Cw0AyBfr2DVQ52oLuBhilD+ryzP+2NMcm6C3vuT0kcAadRs0kIMGaQXmEO4LBhs1R1GTAfahqV58U9jcouBxwAPjXzHJ5V5xWKiUYG02pR3lHbmbb8quhRJI+nyOirSX2hQ9vxB+HCuvyWvJpRy5sCRPNNeGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":534,"cache_read_input_tokens":109895,"cache_creation":{"ephemeral_5m_input_tokens":534,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b79789ec-65b2-4528-a9f3-2ecbb0779dc1"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLHepUidQGiJWL5Ma2p","type":"message","role":"assistant","content":[{"type":"text","text":"Modules gestionnaire terminés et validés en live. Passage au câblage API/CLI/watch."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":534,"cache_read_input_tokens":109895,"cache_creation":{"ephemeral_5m_input_tokens":534,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1664cfce-cf1b-44a5-a188-656dbe994018"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLHepUidQGiJWL5Ma2p","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015XmsMeVjEADTGML4GZtahK","name":"TodoWrite","input":{"todos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"completed","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"in_progress","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Build et redéploiement"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":534,"cache_read_input_tokens":109895,"cache_creation":{"ephemeral_5m_input_tokens":534,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"55c33bca-b7b4-4bac-84e6-b27930478112"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015XmsMeVjEADTGML4GZtahK","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3f851bd8-3f82-45ee-a3e1-bbdaaf274f8b","timestamp":"2026-08-24T22:06:34.483Z","tool_use_result":{"oldTodos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"in_progress","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"pending","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Commit et push"}],"newTodos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"completed","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"in_progress","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Build et redéploiement"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"d0405be2-68c4-4af9-b357-ed8ac4d1f126","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLJccoAUr4KpU3WAo2n","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01AbSw1sL71i9CBgcR5A6unr","name":"Bash","input":{"command":"grep -n \"def \\|@app\\.\" louka/web.py | head -60; echo ---; grep -n \"def main\\|elif cmd\\|if cmd\\|sys.argv\" run.py | head -30","description":"Map web.py endpoints and run.py CLI structure"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1041,"cache_read_input_tokens":110429,"cache_creation":{"ephemeral_5m_input_tokens":1041,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bd67d8f0-1e17-42d4-8245-6d43ae721dbc"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01AbSw1sL71i9CBgcR5A6unr","type":"tool_result","content":"53:def _row_to_dict(row) -> dict:\n80:@app.get(\"/api/listings\")\n81:def list_listings(\n161:@app.get(\"/api/listings.geojson\")\n162:def listings_geojson(\n294:def _parse_poly(poly: str) -> list[tuple[float, float]]:\n305:def _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n318:@app.get(\"/api/search\")\n319:def search_unified(\n467:@app.get(\"/api/img\")\n468:def img_thumbnail(u: str, w: int = 480):\n511:@app.get(\"/api/listings/{uid}/pdf\")\n512:def listing_pdf(uid: str):\n524:@app.get(\"/api/stats/rapport.pdf\")\n525:def rapport_marche_pdf():\n534:@app.get(\"/api/fairvalue/{uid}\")\n535:def fairvalue_detail(uid: str):\n545:@app.get(\"/api/hydro\")\n546:def hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n557:@app.get(\"/api/commerces\")\n558:def commerces_at(lat: float, lng: float):\n564:@app.get(\"/api/air\")\n565:def air_at(lat: float, lng: float):\n574:@app.get(\"/api/gaz\")\n575:def gaz_at(lat: float, lng: float):\n581:@app.get(\"/api/inondation\")\n582:def inondation_at(lat: float, lng: float):\n592:@app.get(\"/api/rdl\")\n593:def rdl_nearby(lat: float, lng: float, radius: int = 600):\n604:@app.get(\"/api/tal\")\n605:def tal_history(address: str, city: str | None = None):\n613:@app.get(\"/api/listings/{uid}\")\n614:def get_listing(uid: str):\n664:@app.get(\"/api/kascores/stats\")\n665:def kascores_stats():\n671:@app.get(\"/api/facets\")\n672:def facets(city: str | None = None):\n703:@app.get(\"/api/sources\")\n704:def sources():\n733:@app.get(\"/api/stats\")\n734:def stats(syncs_since_h: float | None = None):\n778:@app.get(\"/api/stats/detailed\")\n779:def stats_detailed():\n789:@app.get(\"/api/stats/dashboard\")\n790:def stats_dashboard(period: str = \"30j\",\n800:@app.get(\"/api/stats/report\")\n801:def stats_report(period: str = \"30j\",\n826:@app.get(\"/api/stats/catalog\")\n827:def stats_catalog(period: str = \"30j\",\n840:@app.post(\"/api/stats/report/custom\")\n841:def stats_report_custom(spec: dict = Body(...)):\n871:@app.post(\"/api/sync\")\n872:def trigger_sync(background: BackgroundTasks, source: str | None = None):\n874: def _job():\n892: @app.get(\"/doc\")\n893: def doc_redirect():\n896: @app.get(\"/doc/\")\n897: def doc_index():\n900: @app.middleware(\"http\")\n901: async def _cache_headers(request, call_next):\n---\n35:def main() -> None:\n36: cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n37: if cmd == \"sync\":\n39: ingest.run(sys.argv[2:] or None)\n40: elif cmd == \"watch\":\n42: minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n44: elif cmd == \"sync-ct\":\n46: ct_ingest.run(sys.argv[2:] or None)\n47: elif cmd == \"watch-ct\":\n49: minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n51: elif cmd == \"geocode1\":\n53: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n55: elif cmd == \"geocode\":\n58: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n60: elif cmd == \"quality\":\n63: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n65: elif cmd == \"imgaudit\":\n69: limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n70: src = sys.argv[3] if len(sys.argv) > 3 else None\n72: elif cmd == \"fairvalue\":\n76: elif cmd == \"poi\":\n78: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n80: elif cmd == \"quartier\":\n82: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n84: elif cmd == \"env\":\n88: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n90: elif cmd == \"kascores\":\n94: force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n96: elif cmd == \"inondation-build\":\n99: elif cmd == \"air-refresh\":","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5e60b3c7-745a-401f-bf8b-670792f0ddce","timestamp":"2026-08-24T22:06:39.637Z","tool_use_result":{"stdout":"53:def _row_to_dict(row) -> dict:\n80:@app.get(\"/api/listings\")\n81:def list_listings(\n161:@app.get(\"/api/listings.geojson\")\n162:def listings_geojson(\n294:def _parse_poly(poly: str) -> list[tuple[float, float]]:\n305:def _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n318:@app.get(\"/api/search\")\n319:def search_unified(\n467:@app.get(\"/api/img\")\n468:def img_thumbnail(u: str, w: int = 480):\n511:@app.get(\"/api/listings/{uid}/pdf\")\n512:def listing_pdf(uid: str):\n524:@app.get(\"/api/stats/rapport.pdf\")\n525:def rapport_marche_pdf():\n534:@app.get(\"/api/fairvalue/{uid}\")\n535:def fairvalue_detail(uid: str):\n545:@app.get(\"/api/hydro\")\n546:def hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n557:@app.get(\"/api/commerces\")\n558:def commerces_at(lat: float, lng: float):\n564:@app.get(\"/api/air\")\n565:def air_at(lat: float, lng: float):\n574:@app.get(\"/api/gaz\")\n575:def gaz_at(lat: float, lng: float):\n581:@app.get(\"/api/inondation\")\n582:def inondation_at(lat: float, lng: float):\n592:@app.get(\"/api/rdl\")\n593:def rdl_nearby(lat: float, lng: float, radius: int = 600):\n604:@app.get(\"/api/tal\")\n605:def tal_history(address: str, city: str | None = None):\n613:@app.get(\"/api/listings/{uid}\")\n614:def get_listing(uid: str):\n664:@app.get(\"/api/kascores/stats\")\n665:def kascores_stats():\n671:@app.get(\"/api/facets\")\n672:def facets(city: str | None = None):\n703:@app.get(\"/api/sources\")\n704:def sources():\n733:@app.get(\"/api/stats\")\n734:def stats(syncs_since_h: float | None = None):\n778:@app.get(\"/api/stats/detailed\")\n779:def stats_detailed():\n789:@app.get(\"/api/stats/dashboard\")\n790:def stats_dashboard(period: str = \"30j\",\n800:@app.get(\"/api/stats/report\")\n801:def stats_report(period: str = \"30j\",\n826:@app.get(\"/api/stats/catalog\")\n827:def stats_catalog(period: str = \"30j\",\n840:@app.post(\"/api/stats/report/custom\")\n841:def stats_report_custom(spec: dict = Body(...)):\n871:@app.post(\"/api/sync\")\n872:def trigger_sync(background: BackgroundTasks, source: str | None = None):\n874: def _job():\n892: @app.get(\"/doc\")\n893: def doc_redirect():\n896: @app.get(\"/doc/\")\n897: def doc_index():\n900: @app.middleware(\"http\")\n901: async def _cache_headers(request, call_next):\n---\n35:def main() -> None:\n36: cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n37: if cmd == \"sync\":\n39: ingest.run(sys.argv[2:] or None)\n40: elif cmd == \"watch\":\n42: minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n44: elif cmd == \"sync-ct\":\n46: ct_ingest.run(sys.argv[2:] or None)\n47: elif cmd == \"watch-ct\":\n49: minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n51: elif cmd == \"geocode1\":\n53: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n55: elif cmd == \"geocode\":\n58: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n60: elif cmd == \"quality\":\n63: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n65: elif cmd == \"imgaudit\":\n69: limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n70: src = sys.argv[3] if len(sys.argv) > 3 else None\n72: elif cmd == \"fairvalue\":\n76: elif cmd == \"poi\":\n78: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n80: elif cmd == \"quartier\":\n82: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n84: elif cmd == \"env\":\n88: limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n90: elif cmd == \"kascores\":\n94: force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n96: elif cmd == \"inondation-build\":\n99: elif cmd == \"air-refresh\":","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6dd67640-d912-4011-89b8-0ec7c7584722","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLJynGj3XLdGjomvL6d","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015cuAjx5jZVhq8hFS82gyMg","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","offset":604,"limit":70},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1880,"cache_read_input_tokens":111470,"cache_creation":{"ephemeral_5m_input_tokens":1880,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e2efcaaa-ea15-4d37-b350-68c1933d19d8"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015cuAjx5jZVhq8hFS82gyMg","type":"tool_result","content":"604\t@app.get(\"/api/tal\")\n605\tdef tal_history(address: str, city: str | None = None):\n606\t \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n607\t à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n608\t Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n609\t from . import tal\n610\t return tal.lookup(address, city)\n611\t\n612\t\n613\t@app.get(\"/api/listings/{uid}\")\n614\tdef get_listing(uid: str):\n615\t con = db.connect()\n616\t row = con.execute(\n617\t \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n618\t \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n619\t \" fv.confidence AS fv_confidence\"\n620\t \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n621\t \" WHERE uid=?\", (uid,)).fetchone()\n622\t d = None\n623\t if row is not None:\n624\t d = _row_to_dict(row)\n625\t # commodités de proximité (cache par immeuble, voir louka/poi.py)\n626\t if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n627\t key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n628\t poi_row = con.execute(\n629\t \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n630\t d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n631\t else:\n632\t d[\"poi\"] = []\n633\t # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n634\t if d.get(\"coord_key\"):\n635\t ks = con.execute(\n636\t \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n637\t \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n638\t (d[\"coord_key\"],)).fetchone()\n639\t if ks is not None:\n640\t d[\"kascores\"] = {\n641\t \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n642\t \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n643\t \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n644\t \"details\": json.loads(ks[\"details\"] or \"{}\"),\n645\t \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n646\t }\n647\t # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n648\t from . import quartier\n649\t dauid = d.get(\"dauid\")\n650\t d[\"quartier\"] = quartier.fiche_quartier(\n651\t d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n652\t dauid if dauid and dauid != \"hors-zone\" else None)\n653\t # description structurée + historique de prix\n654\t d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n655\t d[\"price_history\"] = [dict(r) for r in con.execute(\n656\t \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n657\t (uid,)).fetchall()]\n658\t con.close()\n659\t if d is None:\n660\t raise HTTPException(404, \"Annonce introuvable\")\n661\t return d\n662\t\n663\t\n664\t@app.get(\"/api/kascores/stats\")\n665\tdef kascores_stats():\n666\t \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n667\t from . import kascores\n668\t return kascores.stats()\n669\t\n670\t\n671\t@app.get(\"/api/facets\")\n672\tdef facets(city: str | None = None):\n673\t \"\"\"Valeurs distinctes pour construire les filtres du frontend."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cb18796d-bbec-4e6e-a4b6-3680606cd2af","timestamp":"2026-08-24T22:06:43.384Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","content":"@app.get(\"/api/tal\")\ndef tal_history(address: str, city: str | None = None):\n \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n from . import tal\n return tal.lookup(address, city)\n\n\n@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):\n con = db.connect()\n row = con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE uid=?\", (uid,)).fetchone()\n d = None\n if row is not None:\n d = _row_to_dict(row)\n # commodités de proximité (cache par immeuble, voir louka/poi.py)\n if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n poi_row = con.execute(\n \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n else:\n d[\"poi\"] = []\n # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n if d.get(\"coord_key\"):\n ks = con.execute(\n \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n (d[\"coord_key\"],)).fetchone()\n if ks is not None:\n d[\"kascores\"] = {\n \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n \"details\": json.loads(ks[\"details\"] or \"{}\"),\n \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n }\n # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n from . import quartier\n dauid = d.get(\"dauid\")\n d[\"quartier\"] = quartier.fiche_quartier(\n d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n d[\"price_history\"] = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n if d is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return d\n\n\n@app.get(\"/api/kascores/stats\")\ndef kascores_stats():\n \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n from . import kascores\n return kascores.stats()\n\n\n@app.get(\"/api/facets\")\ndef facets(city: str | None = None):\n \"\"\"Valeurs distinctes pour construire les filtres du frontend.","numLines":70,"startLine":604,"totalLines":932}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"bb81e06d-349e-4d52-8bdc-5c86e42bb389","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLKG164hfn1eMYkfjG8","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIShwkKpwEIERgCKkCczULravlt1LSrmo7Wa7Bz9hXk8D/C3kB3JEy6SO4xmoAG3lv4GvHS2Y8h5FGibkSgDvMZy4hxCNYx4LYRLegBMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH7gLPUBrABAhIMhn0IqrhAoV5ICTF1GgxCLDRjbYbeKabP/0EiMHREnqaDZaHJA3o/reK8oIG/jA+HpP8bb/0LF9sS86YZSjDq07DRTnocFWg+AjTkfCqMBzXMK41ExQ6d03szY+Yz6VMvAs06vnGGUDn1rArj3ZIa1sXKZFNySHDJGq914naJ9s7OmA1ny+TUmW9fGWa2GRNFNZLiaROU1kfC9pZ3NsK5rH7jDP9fARW3fFkN76H7LpWT4WAY9PAazNtKekUOiZ6VdMMSgQoy8O7IXw2M07ayWBTQU64UmPG21pdXUDFe/zwcrtqSgfb4TodpGKuqkHURMxBQyhnZ3kTzX8Da3zb8rwIZM7rwZDLDU0L83lfkWRXZr3ycRh0JBcjZsbed8sc3b3Bu2Qau14vQB9wU8F3kInHO4EfAQQUW+r3iZIpGyfmeAaf1R4UjjFPDBoCVRnkTfzxwNaqrMSJxn+uzm1IsNDmoFs41S7yQDMRAZW2RFhrjhLL7DkFF/y5y/B+Dixiwzl9G0Xu/z5i+IyTstBINXxmgtBzmKT3Gu7eLeoKYYMMToKNEU3cThLw5o6tS+Xcg4Kafb3Ot+bJnadYVT6xpRtqvkaWEcWA4O1DYXdJxUYNVmqf9WOZResNmpwXsKnblvJqfgDLzHJ6RkSgJCDVCvfg0051Jl6g11TtZ3FYly4syE6k7365PHQuB/zMyLpctd9N5sCPlSbNve6RIMbABHMhP8Wp+O6UAleeHbTS8z/LNQkiwnY2QiDB0pAAegOvCnGALnsjrSvSE0OjT+a8z7HAXupf4M3oITgjk7/uld6eAv+rqRvgxUTghEHHY0TufiDa0l8LlY1nRk8Xc2EmJIoVK7SrForw3wuBr8EdYPUfI6BjFXLs/C0vGi/jjUYKuZiJ5PmlH+OVPab8D+KxpdW1gRDs1qyLHSw+5zym/CyDCZiEfUkOw/lkWBVPaZtjR8VoLq2r6gWCp++Tcefjtxu7BvDXx6dMVEZabpwc+I5n75x7n/jM8qBDIdTsb7QA/JjmyKPouK9HG8DlW8V6nEG4wzdNBQNZtYR9z0wntHmyh/A/fcVpOY/fKEuV3fmeHJmr750qN4mNU86S7ImzZiq9Y01S2NZ5i4NiLpbr59NsTqGUKvn/pwfXJSMMVfpB836QB37DBAH3XGafIE59faUh8gie6Qcve8Nl4ZalRYUCZ2RADeinGhMTna/+gomIdk9bmOjhs3o2XEYJBTXFeSgTeww05yeR1I4SWccNPzIIThpsTbRM6NK2wwQ7yZTLUG/nCN2+X39WqOSS9nH4ttWHP2OiAWVmZTEIzGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1506,"cache_read_input_tokens":113350,"cache_creation":{"ephemeral_5m_input_tokens":1506,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9fafd6e8-677c-4ea3-9850-e43116e03663"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLKG164hfn1eMYkfjG8","type":"message","role":"assistant","content":[{"type":"text","text":"J'ajoute les endpoints dans web.py et les embeds `immeuble`/`hiver` dans la fiche."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1506,"cache_read_input_tokens":113350,"cache_creation":{"ephemeral_5m_input_tokens":1506,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"492baf04-a3b7-4fe4-93f5-65f7f5ecd468"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLKG164hfn1eMYkfjG8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UWBob96kmMraA3ybn8voeg","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","old_string":"@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):","new_string":"@app.get(\"/api/listings/{uid}/historique\")\ndef listing_history(uid: str):\n \"\"\"Historique Lou-Ka : timeline des observations réelles (prix, texte,\n photos, disponibilité, retraits/retours) — voir louka/history.py.\"\"\"\n from . import history\n out = history.timeline(uid)\n if out is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return out\n\n\n@app.get(\"/api/listings/{uid}/cout-reel\")\ndef listing_cout_reel(uid: str):\n \"\"\"Coût réel mensuel : loyer + frais non inclus ligne par ligne, chaque\n poste étiqueté included/observed/estimated/unknown — louka/coutreel.py.\"\"\"\n from . import coutreel\n out = coutreel.compute(uid)\n if out is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return out\n\n\n@app.get(\"/api/listings/{uid}/recyclees\")\ndef listing_recycled(uid: str):\n \"\"\"Annonces antérieures probables du même logement (score multi-signaux,\n inférence explicable) — louka/recycled.py.\"\"\"\n from . import recycled\n return recycled.lookup(uid)\n\n\n@app.get(\"/api/immeuble\")\ndef immeuble_fiche(bkey: str):\n \"\"\"Passeport de l'immeuble (stats précalculées par building.rollup).\"\"\"\n from . import building\n out = building.fiche(bkey)\n if out is None:\n raise HTTPException(404, \"Immeuble inconnu\")\n return out\n\n\n@app.get(\"/api/managers/{source_id}\")\ndef manager_fiche(source_id: str):\n \"\"\"Fiche du gestionnaire (source directe) + avis Google synchronisés en\n base — aucun appel SerpApi au chargement de page (louka/managers.py).\"\"\"\n from . import managers\n out = managers.fiche(source_id)\n if out is None:\n raise HTTPException(404, \"Gestionnaire inconnu\")\n return out\n\n\n@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1506,"cache_read_input_tokens":113350,"cache_creation":{"ephemeral_5m_input_tokens":1506,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"39fde176-47cc-4a6a-9a61-f47307203caf"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UWBob96kmMraA3ybn8voeg","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/web.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"53cc2ff9-91de-4fac-aa89-2edae4ad471c","timestamp":"2026-08-24T22:06:59.296Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","oldString":"@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):","newString":"@app.get(\"/api/listings/{uid}/historique\")\ndef listing_history(uid: str):\n \"\"\"Historique Lou-Ka : timeline des observations réelles (prix, texte,\n photos, disponibilité, retraits/retours) — voir louka/history.py.\"\"\"\n from . import history\n out = history.timeline(uid)\n if out is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return out\n\n\n@app.get(\"/api/listings/{uid}/cout-reel\")\ndef listing_cout_reel(uid: str):\n \"\"\"Coût réel mensuel : loyer + frais non inclus ligne par ligne, chaque\n poste étiqueté included/observed/estimated/unknown — louka/coutreel.py.\"\"\"\n from . import coutreel\n out = coutreel.compute(uid)\n if out is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return out\n\n\n@app.get(\"/api/listings/{uid}/recyclees\")\ndef listing_recycled(uid: str):\n \"\"\"Annonces antérieures probables du même logement (score multi-signaux,\n inférence explicable) — louka/recycled.py.\"\"\"\n from . import recycled\n return recycled.lookup(uid)\n\n\n@app.get(\"/api/immeuble\")\ndef immeuble_fiche(bkey: str):\n \"\"\"Passeport de l'immeuble (stats précalculées par building.rollup).\"\"\"\n from . import building\n out = building.fiche(bkey)\n if out is None:\n raise HTTPException(404, \"Immeuble inconnu\")\n return out\n\n\n@app.get(\"/api/managers/{source_id}\")\ndef manager_fiche(source_id: str):\n \"\"\"Fiche du gestionnaire (source directe) + avis Google synchronisés en\n base — aucun appel SerpApi au chargement de page (louka/managers.py).\"\"\"\n from . import managers\n out = managers.fiche(source_id)\n if out is None:\n raise HTTPException(404, \"Gestionnaire inconnu\")\n return out\n\n\n@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# web.py : API FastAPI (JSON) + service du frontend React (frontend/dist)\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport threading\nfrom pathlib import Path\n\nfrom fastapi import BackgroundTasks, Body, FastAPI, HTTPException, Query\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.middleware.gzip import GZipMiddleware\nfrom fastapi.responses import FileResponse, RedirectResponse\nfrom fastapi.staticfiles import StaticFiles\n\nfrom . import auth, db, ingest\n\nROOT = Path(__file__).resolve().parent.parent\nSOURCES_PATH = ROOT / \"data\" / \"sources.json\"\nFRONTEND_DIST = ROOT / \"frontend\" / \"dist\"\n\napp = FastAPI(title=\"Lou-Ka API\", version=\"1.0\",\n description=\"Agrégateur de logements à louer — province de Québec\")\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n allow_methods=[\"*\"], allow_headers=[\"*\"])\napp.add_middleware(GZipMiddleware, minimum_size=1000)\n\n_sync_lock = threading.Lock()\n\n# comptes utilisateurs (connexion Google) — voir louka/auth.py\napp.include_router(auth.router)\n\n# rôles, favoris, pages gestionnaires — voir louka/accounts.py\nfrom . import accounts # noqa: E402 (import tardif : évite le cycle web<->accounts)\napp.include_router(accounts.router)\n\n# personnalisation du profil (bio, réseaux sociaux, photo) — louka/profile.py\nfrom . import profile as user_profile # noqa: E402\napp.include_router(user_profile.router)\n\n# section « Location court terme » — sous-système isolé (louka/shortterm/)\nfrom .shortterm import web as ct_web # noqa: E402\napp.include_router(ct_web.router)\n\n# fichiers téléversés (photos de profil) — data/uploads/\n_UPLOADS = ROOT / \"data\" / \"uploads\"\n_UPLOADS.mkdir(parents=True, exist_ok=True)\napp.mount(\"/uploads\", StaticFiles(directory=_UPLOADS), name=\"uploads\")\n\n\ndef _row_to_dict(row) -> dict:\n d = dict(row)\n d[\"amenities\"] = json.loads(d.get(\"amenities\") or \"[]\")\n d[\"images\"] = json.loads(d.get(\"images\") or \"[]\")\n # galerie nettoyée par le contrôle qualité (imgcheck.py) : liens morts,\n # minuscules, placeholders et doublons retirés — prime sur la galerie brute\n imgs_ok = d.pop(\"images_ok\", None)\n if imgs_ok:\n try:\n cleaned = json.loads(imgs_ok)\n if isinstance(cleaned, list):\n d[\"images\"] = cleaned\n except (ValueError, TypeError):\n pass\n d.pop(\"img_audit\", None)\n d[\"details\"] = json.loads(d.get(\"details\") or \"{}\")\n # autres plateformes où l'annonce est aussi publiée (badge « aussi sur… »)\n if d.get(\"dup_sources\"):\n try:\n d[\"dup_sources\"] = json.loads(d[\"dup_sources\"])\n except (ValueError, TypeError):\n d[\"dup_sources\"] = []\n if d.get(\"furnished\") is not None:\n d[\"furnished\"] = bool(d[\"furnished\"])\n return d\n\n\n@app.get(\"/api/listings\")\ndef list_listings(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None, # \"oui\" -> oui OU conditions\n furnished: int | None = None, # 1 / 0\n available_by: str | None = None, # ISO : dispo maintenant ou avant date\n area_min: float | None = None, # superficie minimale (pi²)\n q: str | None = None,\n deal: str | None = None, # sous | marche | sur (fair value)\n kascore_min: float | None = None, # KA Score global minimal\n sort: str | None = None, # \"deal\" affaires | \"ka\" KA Score\n active: int = 1,\n limit: int = Query(500, le=2000),\n offset: int = 0,\n):\n con = db.connect()\n sql = (\"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n \" WHERE dup_of IS NULL\") # doublons masqués\n args: list = []\n if active in (0, 1):\n sql += \" AND active=?\"; args.append(active)\n if active == 1: # quarantaine qualité : jamais dans les résultats publics\n sql += \" AND published=1\"\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n # « now » trié après les dates ISO : comparaison explicite\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n if deal in (\"sous\", \"marche\", \"sur\"):\n sql += \" AND fv.verdict=?\"; args.append(deal)\n if kascore_min is not None:\n sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n args.append(kascore_min)\n total = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n if sort == \"deal\": # meilleures affaires d'abord (écart le plus négatif)\n sql += (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n elif sort == \"ka\": # meilleurs emplacements d'abord (KA Score global)\n sql += (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n elif sort == \"prix\":\n sql += \" ORDER BY price IS NULL, price ASC LIMIT ? OFFSET ?\"\n else: # défaut : les plus récentes d'abord\n sql += \" ORDER BY first_seen DESC, price IS NULL, price ASC LIMIT ? OFFSET ?\"\n args += [limit, offset]\n rows = [_row_to_dict(r) for r in con.execute(sql, args).fetchall()]\n con.close()\n return {\"total\": total, \"count\": len(rows), \"listings\": rows}\n\n\n@app.get(\"/api/listings.geojson\")\ndef listings_geojson(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None,\n furnished: int | None = None,\n available_by: str | None = None,\n area_min: float | None = None,\n q: str | None = None,\n bbox: str | None = None,\n limit: int = Query(3000, le=8000),\n):\n \"\"\"Annonces géolocalisées au format GeoJSON (mêmes filtres que /api/listings).\n\n Léger par conception : seuls les champs nécessaires aux marqueurs et aux\n fiches de la carte sont inclus (1 photo, pas de description).\n\n Ka Maps : `bbox=ouest,sud,est,nord` restreint au rectangle visible\n (index idx_listings_geo) ; `limit` plafonne la réponse. La collection\n porte deux membres additionnels : `totalGeocoded` (annonces géolocalisées\n correspondant aux filtres) et `totalMatching` (avec ou sans coordonnées),\n pour afficher « N résultats sans position sur la carte ».\n \"\"\"\n con = db.connect()\n sql = (\"SELECT uid, title, address, price, price_label, unit_type,\"\n \" availability_date, source, city, sector, area_sqft, images,\"\n \" images_ok, lat, lng, fv.verdict AS fv_verdict,\"\n \" fv.deviation AS fv_deviation\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND lat IS NOT NULL AND lng IS NOT NULL\")\n args: list = []\n if bbox:\n try:\n west, south, east, north = (float(v) for v in bbox.split(\",\"))\n except ValueError:\n raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n sql += \" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\"\n args += [south, north, west, east]\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n\n total_geo = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n # Même requête sans l'exigence de coordonnées ni le bbox : mesure les\n # annonces filtrées invisibles sur la carte (affordance « sans position »).\n sql_all = sql.replace(\" AND lat IS NOT NULL AND lng IS NOT NULL\", \"\")\n args_all = list(args)\n if bbox:\n sql_all = sql_all.replace(\" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\", \"\")\n del args_all[0:4]\n total_all = con.execute(f\"SELECT COUNT(*) c FROM ({sql_all})\", args_all).fetchone()[\"c\"]\n\n sql += \" ORDER BY first_seen DESC LIMIT ?\"\n args.append(limit)\n\n features = []\n for r in con.execute(sql, args).fetchall():\n # galerie nettoyée si auditée (même vide : les images brutes sont\n # alors toutes mortes/placeholder — le frontend affiche le visuel\n # de secours Lou-Ka plutôt qu'une image cassée)\n images = json.loads((r[\"images_ok\"] if r[\"images_ok\"] is not None\n else r[\"images\"]) or \"[]\")\n features.append({\n \"type\": \"Feature\",\n \"geometry\": {\"type\": \"Point\", \"coordinates\": [r[\"lng\"], r[\"lat\"]]},\n \"properties\": {\n \"uid\": r[\"uid\"], \"title\": r[\"title\"], \"address\": r[\"address\"],\n \"price\": r[\"price\"],\n \"price_label\": r[\"price_label\"], \"unit_type\": r[\"unit_type\"],\n \"availability_date\": r[\"availability_date\"],\n \"source\": r[\"source\"], \"city\": r[\"city\"], \"sector\": r[\"sector\"],\n \"area_sqft\": r[\"area_sqft\"],\n \"fv_verdict\": r[\"fv_verdict\"], \"fv_deviation\": r[\"fv_deviation\"],\n \"image\": images[0] if images else None,\n },\n })\n con.close()\n return {\"type\": \"FeatureCollection\", \"features\": features,\n \"totalGeocoded\": total_geo, \"totalMatching\": total_all}\n\n\n# --- Recherche unifiée liste + carte -----------------------------------------\n# Une seule vérité partagée : la même requête sert la page de liste ET les\n# points de la carte, dans la même réponse. Le compteur affiché en tête de\n# liste est par construction égal au nombre de points envoyés à la carte.\n\n_SEARCH_SORTS = {\n \"prix\": \" ORDER BY price IS NULL, price ASC\",\n \"prix_desc\": \" ORDER BY price IS NULL, price DESC\",\n \"recent\": \" ORDER BY first_seen DESC, price IS NULL, price ASC\",\n \"deal\": (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n \" price IS NULL, price ASC\"),\n \"ka\": (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n \" price IS NULL, price ASC\"),\n}\n\n# colonnes KA Scores jointes aux annonces (pastilles + tri + fiche)\n_KS_COLS = (\" ks.walk AS ks_walk, ks.transit AS ks_transit,\"\n \" ks.bike AS ks_bike, ks.calme AS ks_calme,\"\n \" ks.services AS ks_services, ks.global AS ks_global\")\n_KS_JOIN = \" LEFT JOIN kascores ks ON ks.coord_key = listings.coord_key\"\n_FV_CODES = {\"sous\": \"s\", \"marche\": \"m\", \"sur\": \"o\"}\n\n\ndef _parse_poly(poly: str) -> list[tuple[float, float]]:\n \"\"\"`poly=lng,lat;lng,lat;…` → liste de sommets (≥ 3) ou HTTP 400.\"\"\"\n try:\n pts = [tuple(float(v) for v in p.split(\",\")) for p in poly.split(\";\") if p]\n except ValueError:\n raise HTTPException(400, \"poly attendu : lng,lat;lng,lat;…\")\n if len(pts) < 3 or any(len(p) != 2 for p in pts):\n raise HTTPException(400, \"poly : au moins 3 sommets lng,lat\")\n return pts # type: ignore[return-value]\n\n\ndef _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n \"\"\"Ray casting — même algorithme que ka-maps (utils/geo.pointInPolygon).\"\"\"\n inside = False\n j = len(ring) - 1\n for i in range(len(ring)):\n xi, yi = ring[i]\n xj, yj = ring[j]\n if (yi > lat) != (yj > lat) and lng < (xj - xi) * (lat - yi) / (yj - yi) + xi:\n inside = not inside\n j = i\n return inside\n\n\n@app.get(\"/api/search\")\ndef search_unified(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None,\n furnished: int | None = None,\n available_by: str | None = None,\n area_min: float | None = None,\n q: str | None = None,\n deal: str | None = None, # sous | marche | sur (juste valeur)\n bbox: str | None = None, # ouest,sud,est,nord (zone visible carte)\n poly: str | None = None, # lng,lat;… (zone dessinée)\n kascore_min: float | None = None, # KA Score global minimal (0-100)\n sort: str = \"recent\", # recent (défaut) | prix | prix_desc | deal | ka\n page: int = 1,\n page_size: int = Query(20, ge=1, le=100),\n include: str = \"tout\", # tout | liste (points inchangés côté client)\n):\n \"\"\"Recherche synchronisée liste ↔ carte (une réponse = les deux vues).\n\n Retourne `total` (compteur partagé), `listings` (la page demandée) et\n `points` (TOUS les points correspondants, format compact\n [uid, lng, lat, prix, verdict]) — triés selon `sort`, si bien que la\n position d'un point dans `points` donne directement sa page de liste.\n Seules les annonces géolocalisées participent (la règle d'or : chaque\n annonce de la liste est visible sur la carte) ; `unpositioned` compte\n les annonces filtrées sans coordonnées, pour l'afficher honnêtement.\n \"\"\"\n if sort not in _SEARCH_SORTS:\n raise HTTPException(400, f\"sort inconnu : {sort}\")\n ring = _parse_poly(poly) if poly else None\n\n con = db.connect()\n sql = (\" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n \" WHERE dup_of IS NULL AND active=1 AND published=1\"\n \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\")\n args: list = []\n if bbox:\n try:\n west, south, east, north = (float(v) for v in bbox.split(\",\"))\n except ValueError:\n raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n sql += (\" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\")\n args += [south, north, west, east]\n if ring:\n # préfiltre SQL par l'emprise du polygone, appartenance exacte en aval\n lngs = [p[0] for p in ring]; lats = [p[1] for p in ring]\n sql += (\" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\")\n args += [min(lats), max(lats), min(lngs), max(lngs)]\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n if deal in (\"sous\", \"marche\", \"sur\"):\n sql += \" AND fv.verdict=?\"; args.append(deal)\n if kascore_min is not None:\n sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n args.append(kascore_min)\n\n rows = con.execute(\n \"SELECT uid, listings.lng AS lng, listings.lat AS lat, price,\"\n \" fv.verdict AS v\" + sql + _SEARCH_SORTS[sort],\n args).fetchall()\n if ring:\n rows = [r for r in rows if _point_in_poly(r[\"lng\"], r[\"lat\"], ring)]\n total = len(rows)\n\n # points compacts : [uid, lng, lat, prix, verdict] — l'index dans ce\n # tableau détermine la page (points et liste partagent le même tri)\n points = [[r[\"uid\"], round(r[\"lng\"], 6), round(r[\"lat\"], 6), r[\"price\"],\n _FV_CODES.get(r[\"v\"] or \"\", None)] for r in rows]\n\n # page demandée, ramenée dans les bornes si les filtres l'ont dépassée\n last = max(1, -(-total // page_size))\n page = min(max(1, page), last)\n page_uids = [r[\"uid\"] for r in rows[(page - 1) * page_size: page * page_size]]\n\n listings: list[dict] = []\n if page_uids:\n marks = \",\".join(\"?\" * len(page_uids))\n by_uid = {r[\"uid\"]: _row_to_dict(r) for r in con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n f\" WHERE uid IN ({marks})\", page_uids).fetchall()}\n listings = [by_uid[u] for u in page_uids if u in by_uid]\n\n # annonces filtrées mais sans coordonnées (affichage honnête, hors carte)\n sql_nogeo = sql.replace(\n \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\",\n \" AND (listings.lat IS NULL OR listings.lng IS NULL)\", 1)\n args_nogeo = list(args)\n for spatial in (bbox, poly):\n if spatial:\n sql_nogeo = sql_nogeo.replace(\n \" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\", \"\", 1)\n del args_nogeo[0:4]\n unpositioned = con.execute(\"SELECT COUNT(*) c\" + sql_nogeo,\n args_nogeo).fetchone()[\"c\"]\n con.close()\n\n return {\n \"total\": total, \"page\": page, \"page_size\": page_size, \"sort\": sort,\n \"listings\": listings,\n \"points\": points if include != \"liste\" else None,\n \"unpositioned\": unpositioned,\n }\n\n\n# --- Miniatures optimisées (WebP, taille écran) ------------------------------\n# Sert les images des annonces redimensionnées et compressées (perf mobile) :\n# /api/img?u=&w=480. Seules les URLs déjà vérifiées par le contrôle\n# qualité (table image_checks) sont proxifiées — pas de proxy ouvert. Si\n# l'URL n'est pas (encore) connue : 404, le frontend retombe sur l'original.\n_IMG_WIDTHS = (160, 480, 800, 1280)\n_IMG_CACHE = ROOT / \"data\" / \"imgcache\"\n\n\n@app.get(\"/api/img\")\ndef img_thumbnail(u: str, w: int = 480):\n import hashlib\n import requests as _rq\n from fastapi.responses import Response\n w = min(_IMG_WIDTHS, key=lambda x: abs(x - w))\n if not u.startswith((\"http://\", \"https://\")):\n raise HTTPException(400, \"URL invalide\")\n key = hashlib.sha1(u.encode()).hexdigest()\n _IMG_CACHE.mkdir(parents=True, exist_ok=True)\n path = _IMG_CACHE / f\"{key}_{w}.webp\"\n headers = {\"Cache-Control\": \"public, max-age=2592000, immutable\"}\n if path.exists():\n return Response(path.read_bytes(), media_type=\"image/webp\",\n headers=headers)\n con = db.connect()\n known = con.execute(\"SELECT status FROM image_checks WHERE url=?\",\n (u,)).fetchone()\n con.close()\n if known is None or known[\"status\"] not in (\"ok\",):\n raise HTTPException(404, \"image inconnue du contrôle qualité\")\n try:\n resp = _rq.get(u, timeout=12, headers={\"User-Agent\": \"LouKaImg/1.0\"})\n resp.raise_for_status()\n from io import BytesIO\n from PIL import Image, ImageOps\n img = Image.open(BytesIO(resp.content))\n img = ImageOps.exif_transpose(img)\n if img.mode not in (\"RGB\", \"L\"):\n img = img.convert(\"RGB\")\n if img.width > w:\n img = img.resize((w, max(1, round(img.height * w / img.width))),\n Image.LANCZOS)\n buf = BytesIO()\n img.save(buf, \"WEBP\", quality=80, method=4)\n data = buf.getvalue()\n path.write_bytes(data)\n return Response(data, media_type=\"image/webp\", headers=headers)\n except HTTPException:\n raise\n except Exception:\n raise HTTPException(404, \"image irrécupérable\")\n\n\n@app.get(\"/api/listings/{uid}/pdf\")\ndef listing_pdf(uid: str):\n \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n from fastapi.responses import Response\n from . import pdfgen\n data = pdfgen.fiche_pdf(uid)\n if data is None:\n raise HTTPException(404, \"Annonce introuvable\")\n nom = uid.replace(\":\", \"-\")\n return Response(content=data, media_type=\"application/pdf\", headers={\n \"Content-Disposition\": f'attachment; filename=\"louka-fiche-{nom}.pdf\"'})\n\n\n@app.get(\"/api/stats/rapport.pdf\")\ndef rapport_marche_pdf():\n \"\"\"Rapport global du marché locatif (PDF multi-pages).\"\"\"\n from fastapi.responses import Response\n from . import pdfgen\n return Response(content=pdfgen.rapport_pdf(), media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n 'attachment; filename=\"louka-rapport-marche.pdf\"'})\n\n\n@app.get(\"/api/fairvalue/{uid}\")\ndef fairvalue_detail(uid: str):\n \"\"\"Analyse de prix Lou-Ka : juste valeur, fourchette, confiance,\n distribution du segment (voir louka/fairvalue.py).\"\"\"\n from . import fairvalue\n d = fairvalue.explain(uid)\n if d is None:\n raise HTTPException(404, \"Pas d'estimation pour cette annonce\")\n return d\n\n\n@app.get(\"/api/hydro\")\ndef hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n lat: float | None = None, lng: float | None = None):\n \"\"\"Estimation du coût d'électricité à une adresse (Hydro-Québec).\n Par défaut cache seulement (aucun captcha) ; solve=1 force le calcul.\n uid/lat/lng permettent la lecture cache par annonce et la dérivation du\n code postal (adresses sans CP).\"\"\"\n from . import hydro\n return hydro.estimate(adresse=adresse, uid=uid, lat=lat, lng=lng,\n solve=bool(solve))\n\n\n@app.get(\"/api/commerces\")\ndef commerces_at(lat: float, lng: float):\n \"\"\"Grands commerces + métro/bus les plus proches (Mapbox / OSM).\"\"\"\n from . import commerces\n return commerces.nearby(lat, lng)\n\n\n@app.get(\"/api/air\")\ndef air_at(lat: float, lng: float):\n \"\"\"Qualité de l'air : station RSQAQ la plus proche (MELCCFP).\"\"\"\n from . import air\n d = air.lookup(lat, lng)\n if d is None:\n raise HTTPException(404, \"Base qualité de l'air non disponible\")\n return d\n\n\n@app.get(\"/api/gaz\")\ndef gaz_at(lat: float, lng: float):\n \"\"\"Stations-service à proximité et prix courants (gazquebec.ca).\"\"\"\n from . import gaz\n return gaz.nearby(lat, lng)\n\n\n@app.get(\"/api/inondation\")\ndef inondation_at(lat: float, lng: float):\n \"\"\"Risque d'inondation à l'adresse (BDZI, gouv. du Québec) —\n bloc « Risque d'inondation » de la fiche.\"\"\"\n from . import inondation\n d = inondation.lookup(lat, lng)\n if d is None:\n raise HTTPException(404, \"Base des zones inondables non disponible\")\n return d\n\n\n@app.get(\"/api/rdl\")\ndef rdl_nearby(lat: float, lng: float, radius: int = 600):\n \"\"\"Loyers déclarés au Registre des loyers (registre-des-loyers.ca)\n autour d'un point — bloc « Registre des loyers » de la fiche.\"\"\"\n from . import rdl\n radius = max(100, min(radius, 2000))\n d = rdl.nearby(lat, lng, radius)\n if d is None:\n raise HTTPException(404, \"Registre des loyers non disponible\")\n return d\n\n\n@app.get(\"/api/tal\")\ndef tal_history(address: str, city: str | None = None):\n \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n from . import tal\n return tal.lookup(address, city)\n\n\n@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):\n con = db.connect()\n row = con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE uid=?\", (uid,)).fetchone()\n d = None\n if row is not None:\n d = _row_to_dict(row)\n # commodités de proximité (cache par immeuble, voir louka/poi.py)\n if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n poi_row = con.execute(\n \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n else:\n d[\"poi\"] = []\n # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n if d.get(\"coord_key\"):\n ks = con.execute(\n \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n (d[\"coord_key\"],)).fetchone()\n if ks is not None:\n d[\"kascores\"] = {\n \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n \"details\": json.loads(ks[\"details\"] or \"{}\"),\n \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n }\n # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n from . import quartier\n dauid = d.get(\"dauid\")\n d[\"quartier\"] = quartier.fiche_quartier(\n d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n d[\"price_history\"] = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n if d is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return d\n\n\n@app.get(\"/api/kascores/stats\")\ndef kascores_stats():\n \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n from . import kascores\n return kascores.stats()\n\n\n@app.get(\"/api/facets\")\ndef facets(city: str | None = None):\n \"\"\"Valeurs distinctes pour construire les filtres du frontend.\n\n `city` (optionnel) restreint la liste des quartiers à cette ville —\n utilisé par le sélecteur « Quartier » dépendant de « Ville ».\n \"\"\"\n con = db.connect()\n sector_sql = (\"SELECT DISTINCT sector FROM listings\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND sector<>''\")\n sector_args: list = []\n if city:\n sector_sql += \" AND city=?\"\n sector_args.append(city)\n out = {\n \"cities\": [r[\"city\"] for r in con.execute(\n \"SELECT DISTINCT city FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL AND city<>'' ORDER BY city\")],\n \"sectors\": [r[\"sector\"] for r in con.execute(\n sector_sql + \" ORDER BY sector\", sector_args)],\n \"unit_types\": [r[\"unit_type\"] for r in con.execute(\n \"SELECT DISTINCT unit_type FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL AND unit_type<>'' ORDER BY unit_type\")],\n \"sources\": [dict(r) for r in con.execute(\n \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL GROUP BY source ORDER BY n DESC\")],\n }\n con.close()\n return out\n\n\n@app.get(\"/api/sources\")\ndef sources():\n registry = json.loads(SOURCES_PATH.read_text(encoding=\"utf-8\"))[\"sources\"]\n con = db.connect()\n counts = {r[\"source\"]: r[\"n\"] for r in con.execute(\n \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n \" GROUP BY source\")}\n last = {r[\"source\"]: r[\"ts\"] for r in con.execute(\n \"SELECT source, MAX(ts) ts FROM sync_log WHERE ok=1 GROUP BY source\")}\n con.close()\n for s in registry:\n s[\"active_listings\"] = counts.get(s[\"id\"], 0)\n s[\"last_sync\"] = last.get(s[\"id\"])\n return {\"sources\": registry}\n\n\n# Villes de la grande région de Montréal (le reste de la province est\n# compté dans « autres » — Outaouais, Estrie, Mauricie, Est-du-Québec…)\n_VILLES_GM = (\n \"Montréal\", \"Montréal-Est\", \"Laval\", \"Longueuil\", \"Brossard\", \"Terrebonne\",\n \"Mascouche\", \"Repentigny\", \"Boucherville\", \"Saint-Lambert\", \"Westmount\",\n \"Côte Saint-Luc\", \"Dorval\", \"Pointe-Claire\", \"Verdun\", \"LaSalle\", \"Anjou\",\n \"Saint-Constant\", \"Delson\", \"Candiac\", \"Châteauguay\", \"Chambly\",\n \"Sainte-Julie\", \"Varennes\", \"Belœil\", \"McMasterville\", \"Mont-Saint-Hilaire\",\n \"Saint-Basile-le-Grand\", \"Saint-Bruno-de-Montarville\", \"Boisbriand\",\n \"Richelieu\", \"Beauharnois\", \"Salaberry-de-Valleyfield\", \"Vaudreuil-Dorion\",\n \"Kirkland\", \"Beaconsfield\", \"Dollard-des-Ormeaux\", \"Mont-Royal\", \"Outremont\",\n)\n\n\n@app.get(\"/api/stats\")\ndef stats(syncs_since_h: float | None = None):\n \"\"\"Stats globales + journal de synchronisation.\n\n `syncs_since_h` (optionnel) : fenêtre en heures pour `recent_syncs` — le\n moniteur central (api-ka) polle aux 2 h et les 20 dernières entrées ne\n couvrent que ~11 min d'activité sur ~200 sources (fausses alertes\n « stale »). Avec le paramètre : toutes les entrées des N dernières heures\n (max 4000) ; s'il y en a moins de 20, on retombe sur les 20 dernières\n toutes époques confondues. Sans le paramètre : comportement historique\n inchangé (LIMIT 20).\n \"\"\"\n con = db.connect()\n gm = \",\".join(\"?\" * len(_VILLES_GM))\n row = con.execute(\n f\"\"\"SELECT COUNT(*) total,\n SUM(CASE WHEN city='Québec' THEN 1 ELSE 0 END) quebec,\n SUM(CASE WHEN city='Lévis' THEN 1 ELSE 0 END) levis,\n SUM(CASE WHEN city IN ({gm}) THEN 1 ELSE 0 END) montreal,\n SUM(CASE WHEN city NOT IN ('Québec','Lévis') AND city NOT IN ({gm})\n THEN 1 ELSE 0 END) autres,\n COUNT(DISTINCT source) sources,\n AVG(price) avg_price\n FROM listings WHERE active=1 AND published=1 AND dup_of IS NULL\"\"\",\n list(_VILLES_GM) + list(_VILLES_GM)).fetchone()\n log: list[dict] = []\n if syncs_since_h is not None:\n import time as _time\n cutoff = _time.time() - syncs_since_h * 3600\n log = [dict(r) for r in con.execute(\n \"SELECT * FROM sync_log WHERE ts >= ? ORDER BY ts DESC LIMIT 4000\",\n (cutoff,))]\n if syncs_since_h is None or len(log) < 20:\n log = [dict(r) for r in con.execute(\n \"SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20\")]\n # qualité des données : complétude moyenne + quarantaine (quality.py)\n qual = con.execute(\n \"\"\"SELECT ROUND(AVG(completeness),1) avg_completeness,\n SUM(published=1) publiees, SUM(published=0) quarantaine\n FROM listings WHERE active=1\"\"\"\n ).fetchone()\n con.close()\n return {**dict(row), \"quality\": dict(qual), \"recent_syncs\": log}\n\n\n@app.get(\"/api/stats/detailed\")\ndef stats_detailed():\n \"\"\"Agrégats du marché (source unique : louka/marketstats.py).\"\"\"\n from . import marketstats\n return marketstats.compute()\n\n\n# --- Module Stats commun Groupe KA (contrat frontend/src/ka/stats/SPEC.md) ---\n_PERIODES_VALIDES = {\"auj\", \"7j\", \"30j\", \"3m\", \"6m\", \"12m\", \"annee\", \"tout\"}\n\n\n@app.get(\"/api/stats/dashboard\")\ndef stats_dashboard(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None):\n \"\"\"Tableau de bord analytique (KPI, séries, répartitions, tableaux…).\"\"\"\n from . import statsdash\n if period not in _PERIODES_VALIDES and not (from_ and to):\n period = \"30j\"\n return statsdash.compute(period, from_, to)\n\n\n@app.get(\"/api/stats/report\")\ndef stats_report(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None,\n mode: str = \"complet\"):\n \"\"\"Rapport statistique PDF estampillé Groupe-KA — 5 modes :\n complet | synthese | tendances | repartitions | donnees (inconnu → complet).\"\"\"\n from fastapi.responses import Response\n from . import kapdf, statsdash\n if period not in _PERIODES_VALIDES:\n period = \"perso\" if (from_ and to) else \"30j\"\n if mode not in kapdf.REPORT_MODES:\n mode = \"complet\"\n dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n from_, to)\n eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n data = kapdf.GroupeKAReport(site=site, dashboard=dash, mode=mode).build()\n fname = kapdf.filename(\"lou-ka\", period, mode)\n return Response(content=data, media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n f'attachment; filename=\"{fname}\"'})\n\n\n\n@app.get(\"/api/stats/catalog\")\ndef stats_catalog(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None):\n \"\"\"v3 — blocs composables pour le constructeur de rapports personnalisés.\"\"\"\n from . import kapdf, statsdash\n if period not in _PERIODES_VALIDES and not (from_ and to):\n period = \"30j\"\n dash = statsdash.compute(period if period in _PERIODES_VALIDES else \"30j\",\n from_, to)\n return {\"updated\": dash.get(\"updated\"), \"period\": dash.get(\"period\"),\n \"blocks\": kapdf.catalog(dash)}\n\n\n@app.post(\"/api/stats/report/custom\")\ndef stats_report_custom(spec: dict = Body(...)):\n \"\"\"v3 — rapport PDF personnalisé : {\"title\", \"period\", \"from\", \"to\",\n \"blocks\": [{\"key\": \"series:…\", \"render\": \"bar\"}, …]} (SPEC.md §3bis).\"\"\"\n from fastapi.responses import Response\n from . import kapdf, statsdash\n period = str(spec.get(\"period\") or \"30j\")\n from_ = str(spec.get(\"from\") or \"\") or None\n to = str(spec.get(\"to\") or \"\") or None\n if period not in _PERIODES_VALIDES:\n period = \"perso\" if (from_ and to) else \"30j\"\n dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n from_, to)\n known = {b[\"key\"] for b in kapdf.catalog(dash)}\n blocks = [b for b in (spec.get(\"blocks\") or [])\n if isinstance(b, dict) and b.get(\"key\") in known][:40]\n if not blocks:\n raise HTTPException(400, \"Aucun bloc valide dans la composition\")\n eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n data = kapdf.GroupeKAReport(\n site=site, dashboard=dash, mode=kapdf.CUSTOM_MODE,\n spec={\"title\": str(spec.get(\"title\") or \"\")[:80], \"blocks\": blocks},\n ).build()\n fname = kapdf.filename(\"lou-ka\", period, kapdf.CUSTOM_MODE)\n return Response(content=data, media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n f'attachment; filename=\"{fname}\"'})\n\n\n@app.post(\"/api/sync\")\ndef trigger_sync(background: BackgroundTasks, source: str | None = None):\n \"\"\"Déclenche une synchronisation (équivalent d'un webhook entrant).\"\"\"\n def _job():\n with _sync_lock:\n ingest.run([source] if source else None)\n background.add_task(_job)\n return {\"status\": \"démarré\", \"source\": source or \"toutes\"}\n\n\n# --- Frontend React (build Vite) --------------------------------------------\nif FRONTEND_DIST.exists():\n app.mount(\"/assets\", StaticFiles(directory=FRONTEND_DIST / \"assets\"), name=\"assets\")\n\n # référencement : SSR des routes publiques, robots.txt, sitemaps, 410.\n # Inclus AVANT le catch-all — l'ordre d'enregistrement fait foi.\n from . import seo # noqa: E402\n app.include_router(seo.router)\n\n # Documentation utilisateur (frontend/public/doc → dist/doc) — routes\n # explicites AVANT le catch-all SPA pour servir /doc/ comme un index.\n @app.get(\"/doc\")\n def doc_redirect():\n return RedirectResponse(\"/doc/\", status_code=308)\n\n @app.get(\"/doc/\")\n def doc_index():\n return FileResponse(FRONTEND_DIST / \"doc\" / \"index.html\")\n\n @app.middleware(\"http\")\n async def _cache_headers(request, call_next):\n \"\"\"Politique de cache : les bundles hachés (/assets/…) sont immuables,\n mais index.html doit TOUJOURS être revalidé — sinon les navigateurs\n gardent une vieille version de l'app après un déploiement.\"\"\"\n resp = await call_next(request)\n path = request.url.path\n if path.startswith(\"/assets/\"):\n resp.headers[\"Cache-Control\"] = \"public, max-age=31536000, immutable\"\n elif \"text/html\" in (resp.headers.get(\"content-type\") or \"\"):\n resp.headers[\"Cache-Control\"] = \"no-cache\"\n return resp\n\n # routes de l'app rendues uniquement côté client (privées ou volatiles) —\n # tout autre chemin inconnu renvoie index.html avec un statut 404 : le\n # routeur React affiche sa page « introuvable », les moteurs de recherche\n # voient un vrai 404 (fin des soft-404).\n _CLIENT_ROUTES = {\"profil\", \"favoris\", \"gestion\", \"bienvenue\", \"bot\",\n \"contact\", \"juste-valeur\", \"court-terme\"}\n _CLIENT_PREFIXES = (\"u/\", \"passerelle/\", \"court-terme/\")\n\n @app.get(\"/{full_path:path}\")\n def spa(full_path: str):\n target = FRONTEND_DIST / full_path\n if full_path and target.is_file():\n return FileResponse(target)\n known = (full_path in _CLIENT_ROUTES\n or any(full_path == p.rstrip(\"/\") or full_path.startswith(p)\n for p in _CLIENT_PREFIXES))\n return FileResponse(FRONTEND_DIST / \"index.html\",\n status_code=200 if known else 404,\n headers={\"Cache-Control\": \"no-cache\"})\n","structuredPatch":[{"oldStart":610,"oldLines":6,"newStart":610,"newLines":57,"lines":[" return tal.lookup(address, city)"," "," ","+@app.get(\"/api/listings/{uid}/historique\")","+def listing_history(uid: str):","+ \"\"\"Historique Lou-Ka : timeline des observations réelles (prix, texte,","+ photos, disponibilité, retraits/retours) — voir louka/history.py.\"\"\"","+ from . import history","+ out = history.timeline(uid)","+ if out is None:","+ raise HTTPException(404, \"Annonce introuvable\")","+ return out","+","+","+@app.get(\"/api/listings/{uid}/cout-reel\")","+def listing_cout_reel(uid: str):","+ \"\"\"Coût réel mensuel : loyer + frais non inclus ligne par ligne, chaque","+ poste étiqueté included/observed/estimated/unknown — louka/coutreel.py.\"\"\"","+ from . import coutreel","+ out = coutreel.compute(uid)","+ if out is None:","+ raise HTTPException(404, \"Annonce introuvable\")","+ return out","+","+","+@app.get(\"/api/listings/{uid}/recyclees\")","+def listing_recycled(uid: str):","+ \"\"\"Annonces antérieures probables du même logement (score multi-signaux,","+ inférence explicable) — louka/recycled.py.\"\"\"","+ from . import recycled","+ return recycled.lookup(uid)","+","+","+@app.get(\"/api/immeuble\")","+def immeuble_fiche(bkey: str):","+ \"\"\"Passeport de l'immeuble (stats précalculées par building.rollup).\"\"\"","+ from . import building","+ out = building.fiche(bkey)","+ if out is None:","+ raise HTTPException(404, \"Immeuble inconnu\")","+ return out","+","+","+@app.get(\"/api/managers/{source_id}\")","+def manager_fiche(source_id: str):","+ \"\"\"Fiche du gestionnaire (source directe) + avis Google synchronisés en","+ base — aucun appel SerpApi au chargement de page (louka/managers.py).\"\"\"","+ from . import managers","+ out = managers.fiche(source_id)","+ if out is None:","+ raise HTTPException(404, \"Gestionnaire inconnu\")","+ return out","+","+"," @app.get(\"/api/listings/{uid}\")"," def get_listing(uid: str):"," con = db.connect()"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"9937b3a7-d48e-452c-bb12-a240bf8c8f91","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLLRvvD9nLkYd6ppjeG","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NW3NBRgZy1cLMHKjGeZRyS","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","old_string":" # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None","new_string":" # passeport de l'immeuble (stats précalculées) + score hiver\n if d.get(\"building_key\"):\n from . import building\n d[\"immeuble\"] = building.fiche(d[\"building_key\"], con)\n else:\n d[\"immeuble\"] = None\n from . import hiver\n d[\"hiver\"] = hiver.score(d.get(\"lat\"), d.get(\"lng\"), con)\n # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1341,"cache_read_input_tokens":114856,"cache_creation":{"ephemeral_5m_input_tokens":1341,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2fd00feb-4c7f-4c2a-80f4-650bd4077b01"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NW3NBRgZy1cLMHKjGeZRyS","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/web.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"65b262af-2aab-4be3-bc7a-9e69fbf57a2b","timestamp":"2026-08-24T22:07:06.219Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/web.py","oldString":" # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None","newString":" # passeport de l'immeuble (stats précalculées) + score hiver\n if d.get(\"building_key\"):\n from . import building\n d[\"immeuble\"] = building.fiche(d[\"building_key\"], con)\n else:\n d[\"immeuble\"] = None\n from . import hiver\n d[\"hiver\"] = hiver.score(d.get(\"lat\"), d.get(\"lng\"), con)\n # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# web.py : API FastAPI (JSON) + service du frontend React (frontend/dist)\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport json\nimport threading\nfrom pathlib import Path\n\nfrom fastapi import BackgroundTasks, Body, FastAPI, HTTPException, Query\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.middleware.gzip import GZipMiddleware\nfrom fastapi.responses import FileResponse, RedirectResponse\nfrom fastapi.staticfiles import StaticFiles\n\nfrom . import auth, db, ingest\n\nROOT = Path(__file__).resolve().parent.parent\nSOURCES_PATH = ROOT / \"data\" / \"sources.json\"\nFRONTEND_DIST = ROOT / \"frontend\" / \"dist\"\n\napp = FastAPI(title=\"Lou-Ka API\", version=\"1.0\",\n description=\"Agrégateur de logements à louer — province de Québec\")\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n allow_methods=[\"*\"], allow_headers=[\"*\"])\napp.add_middleware(GZipMiddleware, minimum_size=1000)\n\n_sync_lock = threading.Lock()\n\n# comptes utilisateurs (connexion Google) — voir louka/auth.py\napp.include_router(auth.router)\n\n# rôles, favoris, pages gestionnaires — voir louka/accounts.py\nfrom . import accounts # noqa: E402 (import tardif : évite le cycle web<->accounts)\napp.include_router(accounts.router)\n\n# personnalisation du profil (bio, réseaux sociaux, photo) — louka/profile.py\nfrom . import profile as user_profile # noqa: E402\napp.include_router(user_profile.router)\n\n# section « Location court terme » — sous-système isolé (louka/shortterm/)\nfrom .shortterm import web as ct_web # noqa: E402\napp.include_router(ct_web.router)\n\n# fichiers téléversés (photos de profil) — data/uploads/\n_UPLOADS = ROOT / \"data\" / \"uploads\"\n_UPLOADS.mkdir(parents=True, exist_ok=True)\napp.mount(\"/uploads\", StaticFiles(directory=_UPLOADS), name=\"uploads\")\n\n\ndef _row_to_dict(row) -> dict:\n d = dict(row)\n d[\"amenities\"] = json.loads(d.get(\"amenities\") or \"[]\")\n d[\"images\"] = json.loads(d.get(\"images\") or \"[]\")\n # galerie nettoyée par le contrôle qualité (imgcheck.py) : liens morts,\n # minuscules, placeholders et doublons retirés — prime sur la galerie brute\n imgs_ok = d.pop(\"images_ok\", None)\n if imgs_ok:\n try:\n cleaned = json.loads(imgs_ok)\n if isinstance(cleaned, list):\n d[\"images\"] = cleaned\n except (ValueError, TypeError):\n pass\n d.pop(\"img_audit\", None)\n d[\"details\"] = json.loads(d.get(\"details\") or \"{}\")\n # autres plateformes où l'annonce est aussi publiée (badge « aussi sur… »)\n if d.get(\"dup_sources\"):\n try:\n d[\"dup_sources\"] = json.loads(d[\"dup_sources\"])\n except (ValueError, TypeError):\n d[\"dup_sources\"] = []\n if d.get(\"furnished\") is not None:\n d[\"furnished\"] = bool(d[\"furnished\"])\n return d\n\n\n@app.get(\"/api/listings\")\ndef list_listings(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None, # \"oui\" -> oui OU conditions\n furnished: int | None = None, # 1 / 0\n available_by: str | None = None, # ISO : dispo maintenant ou avant date\n area_min: float | None = None, # superficie minimale (pi²)\n q: str | None = None,\n deal: str | None = None, # sous | marche | sur (fair value)\n kascore_min: float | None = None, # KA Score global minimal\n sort: str | None = None, # \"deal\" affaires | \"ka\" KA Score\n active: int = 1,\n limit: int = Query(500, le=2000),\n offset: int = 0,\n):\n con = db.connect()\n sql = (\"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n \" WHERE dup_of IS NULL\") # doublons masqués\n args: list = []\n if active in (0, 1):\n sql += \" AND active=?\"; args.append(active)\n if active == 1: # quarantaine qualité : jamais dans les résultats publics\n sql += \" AND published=1\"\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n # « now » trié après les dates ISO : comparaison explicite\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n if deal in (\"sous\", \"marche\", \"sur\"):\n sql += \" AND fv.verdict=?\"; args.append(deal)\n if kascore_min is not None:\n sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n args.append(kascore_min)\n total = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n if sort == \"deal\": # meilleures affaires d'abord (écart le plus négatif)\n sql += (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n elif sort == \"ka\": # meilleurs emplacements d'abord (KA Score global)\n sql += (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n \" price IS NULL, price ASC LIMIT ? OFFSET ?\")\n elif sort == \"prix\":\n sql += \" ORDER BY price IS NULL, price ASC LIMIT ? OFFSET ?\"\n else: # défaut : les plus récentes d'abord\n sql += \" ORDER BY first_seen DESC, price IS NULL, price ASC LIMIT ? OFFSET ?\"\n args += [limit, offset]\n rows = [_row_to_dict(r) for r in con.execute(sql, args).fetchall()]\n con.close()\n return {\"total\": total, \"count\": len(rows), \"listings\": rows}\n\n\n@app.get(\"/api/listings.geojson\")\ndef listings_geojson(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None,\n furnished: int | None = None,\n available_by: str | None = None,\n area_min: float | None = None,\n q: str | None = None,\n bbox: str | None = None,\n limit: int = Query(3000, le=8000),\n):\n \"\"\"Annonces géolocalisées au format GeoJSON (mêmes filtres que /api/listings).\n\n Léger par conception : seuls les champs nécessaires aux marqueurs et aux\n fiches de la carte sont inclus (1 photo, pas de description).\n\n Ka Maps : `bbox=ouest,sud,est,nord` restreint au rectangle visible\n (index idx_listings_geo) ; `limit` plafonne la réponse. La collection\n porte deux membres additionnels : `totalGeocoded` (annonces géolocalisées\n correspondant aux filtres) et `totalMatching` (avec ou sans coordonnées),\n pour afficher « N résultats sans position sur la carte ».\n \"\"\"\n con = db.connect()\n sql = (\"SELECT uid, title, address, price, price_label, unit_type,\"\n \" availability_date, source, city, sector, area_sqft, images,\"\n \" images_ok, lat, lng, fv.verdict AS fv_verdict,\"\n \" fv.deviation AS fv_deviation\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND lat IS NOT NULL AND lng IS NOT NULL\")\n args: list = []\n if bbox:\n try:\n west, south, east, north = (float(v) for v in bbox.split(\",\"))\n except ValueError:\n raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n sql += \" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\"\n args += [south, north, west, east]\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n\n total_geo = con.execute(f\"SELECT COUNT(*) c FROM ({sql})\", args).fetchone()[\"c\"]\n # Même requête sans l'exigence de coordonnées ni le bbox : mesure les\n # annonces filtrées invisibles sur la carte (affordance « sans position »).\n sql_all = sql.replace(\" AND lat IS NOT NULL AND lng IS NOT NULL\", \"\")\n args_all = list(args)\n if bbox:\n sql_all = sql_all.replace(\" AND lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?\", \"\")\n del args_all[0:4]\n total_all = con.execute(f\"SELECT COUNT(*) c FROM ({sql_all})\", args_all).fetchone()[\"c\"]\n\n sql += \" ORDER BY first_seen DESC LIMIT ?\"\n args.append(limit)\n\n features = []\n for r in con.execute(sql, args).fetchall():\n # galerie nettoyée si auditée (même vide : les images brutes sont\n # alors toutes mortes/placeholder — le frontend affiche le visuel\n # de secours Lou-Ka plutôt qu'une image cassée)\n images = json.loads((r[\"images_ok\"] if r[\"images_ok\"] is not None\n else r[\"images\"]) or \"[]\")\n features.append({\n \"type\": \"Feature\",\n \"geometry\": {\"type\": \"Point\", \"coordinates\": [r[\"lng\"], r[\"lat\"]]},\n \"properties\": {\n \"uid\": r[\"uid\"], \"title\": r[\"title\"], \"address\": r[\"address\"],\n \"price\": r[\"price\"],\n \"price_label\": r[\"price_label\"], \"unit_type\": r[\"unit_type\"],\n \"availability_date\": r[\"availability_date\"],\n \"source\": r[\"source\"], \"city\": r[\"city\"], \"sector\": r[\"sector\"],\n \"area_sqft\": r[\"area_sqft\"],\n \"fv_verdict\": r[\"fv_verdict\"], \"fv_deviation\": r[\"fv_deviation\"],\n \"image\": images[0] if images else None,\n },\n })\n con.close()\n return {\"type\": \"FeatureCollection\", \"features\": features,\n \"totalGeocoded\": total_geo, \"totalMatching\": total_all}\n\n\n# --- Recherche unifiée liste + carte -----------------------------------------\n# Une seule vérité partagée : la même requête sert la page de liste ET les\n# points de la carte, dans la même réponse. Le compteur affiché en tête de\n# liste est par construction égal au nombre de points envoyés à la carte.\n\n_SEARCH_SORTS = {\n \"prix\": \" ORDER BY price IS NULL, price ASC\",\n \"prix_desc\": \" ORDER BY price IS NULL, price DESC\",\n \"recent\": \" ORDER BY first_seen DESC, price IS NULL, price ASC\",\n \"deal\": (\" ORDER BY fv.deviation IS NULL, fv.deviation ASC,\"\n \" price IS NULL, price ASC\"),\n \"ka\": (\" ORDER BY ks.global IS NULL, ks.global DESC,\"\n \" price IS NULL, price ASC\"),\n}\n\n# colonnes KA Scores jointes aux annonces (pastilles + tri + fiche)\n_KS_COLS = (\" ks.walk AS ks_walk, ks.transit AS ks_transit,\"\n \" ks.bike AS ks_bike, ks.calme AS ks_calme,\"\n \" ks.services AS ks_services, ks.global AS ks_global\")\n_KS_JOIN = \" LEFT JOIN kascores ks ON ks.coord_key = listings.coord_key\"\n_FV_CODES = {\"sous\": \"s\", \"marche\": \"m\", \"sur\": \"o\"}\n\n\ndef _parse_poly(poly: str) -> list[tuple[float, float]]:\n \"\"\"`poly=lng,lat;lng,lat;…` → liste de sommets (≥ 3) ou HTTP 400.\"\"\"\n try:\n pts = [tuple(float(v) for v in p.split(\",\")) for p in poly.split(\";\") if p]\n except ValueError:\n raise HTTPException(400, \"poly attendu : lng,lat;lng,lat;…\")\n if len(pts) < 3 or any(len(p) != 2 for p in pts):\n raise HTTPException(400, \"poly : au moins 3 sommets lng,lat\")\n return pts # type: ignore[return-value]\n\n\ndef _point_in_poly(lng: float, lat: float, ring: list[tuple[float, float]]) -> bool:\n \"\"\"Ray casting — même algorithme que ka-maps (utils/geo.pointInPolygon).\"\"\"\n inside = False\n j = len(ring) - 1\n for i in range(len(ring)):\n xi, yi = ring[i]\n xj, yj = ring[j]\n if (yi > lat) != (yj > lat) and lng < (xj - xi) * (lat - yi) / (yj - yi) + xi:\n inside = not inside\n j = i\n return inside\n\n\n@app.get(\"/api/search\")\ndef search_unified(\n city: str | None = None,\n sector: str | None = None,\n unit_type: str | None = None,\n source: str | None = None,\n price_max: float | None = None,\n price_min: float | None = None,\n pets: str | None = None,\n furnished: int | None = None,\n available_by: str | None = None,\n area_min: float | None = None,\n q: str | None = None,\n deal: str | None = None, # sous | marche | sur (juste valeur)\n bbox: str | None = None, # ouest,sud,est,nord (zone visible carte)\n poly: str | None = None, # lng,lat;… (zone dessinée)\n kascore_min: float | None = None, # KA Score global minimal (0-100)\n sort: str = \"recent\", # recent (défaut) | prix | prix_desc | deal | ka\n page: int = 1,\n page_size: int = Query(20, ge=1, le=100),\n include: str = \"tout\", # tout | liste (points inchangés côté client)\n):\n \"\"\"Recherche synchronisée liste ↔ carte (une réponse = les deux vues).\n\n Retourne `total` (compteur partagé), `listings` (la page demandée) et\n `points` (TOUS les points correspondants, format compact\n [uid, lng, lat, prix, verdict]) — triés selon `sort`, si bien que la\n position d'un point dans `points` donne directement sa page de liste.\n Seules les annonces géolocalisées participent (la règle d'or : chaque\n annonce de la liste est visible sur la carte) ; `unpositioned` compte\n les annonces filtrées sans coordonnées, pour l'afficher honnêtement.\n \"\"\"\n if sort not in _SEARCH_SORTS:\n raise HTTPException(400, f\"sort inconnu : {sort}\")\n ring = _parse_poly(poly) if poly else None\n\n con = db.connect()\n sql = (\" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n \" WHERE dup_of IS NULL AND active=1 AND published=1\"\n \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\")\n args: list = []\n if bbox:\n try:\n west, south, east, north = (float(v) for v in bbox.split(\",\"))\n except ValueError:\n raise HTTPException(400, \"bbox attendu : ouest,sud,est,nord\")\n sql += (\" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\")\n args += [south, north, west, east]\n if ring:\n # préfiltre SQL par l'emprise du polygone, appartenance exacte en aval\n lngs = [p[0] for p in ring]; lats = [p[1] for p in ring]\n sql += (\" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\")\n args += [min(lats), max(lats), min(lngs), max(lngs)]\n if city:\n sql += \" AND city=?\"; args.append(city)\n if sector:\n sql += \" AND sector LIKE ?\"; args.append(f\"%{sector}%\")\n if unit_type:\n sql += \" AND unit_type=?\"; args.append(unit_type)\n if source:\n sql += \" AND source=?\"; args.append(source)\n if price_max is not None:\n sql += \" AND price IS NOT NULL AND price<=?\"; args.append(price_max)\n if price_min is not None:\n sql += \" AND price IS NOT NULL AND price>=?\"; args.append(price_min)\n if pets == \"oui\":\n sql += \" AND pets IN ('oui','conditions')\"\n elif pets:\n sql += \" AND pets=?\"; args.append(pets)\n if furnished in (0, 1):\n sql += \" AND furnished=?\"; args.append(furnished)\n if available_by:\n sql += \" AND (availability_date='now' OR (availability_date IS NOT NULL\" \\\n \" AND availability_date<=?))\"\n args.append(available_by)\n if area_min is not None:\n sql += \" AND area_sqft IS NOT NULL AND area_sqft>=?\"; args.append(area_min)\n if q:\n sql += \" AND (title LIKE ? OR address LIKE ? OR sector LIKE ?)\"\n args += [f\"%{q}%\"] * 3\n if deal in (\"sous\", \"marche\", \"sur\"):\n sql += \" AND fv.verdict=?\"; args.append(deal)\n if kascore_min is not None:\n sql += \" AND ks.global IS NOT NULL AND ks.global>=?\"\n args.append(kascore_min)\n\n rows = con.execute(\n \"SELECT uid, listings.lng AS lng, listings.lat AS lat, price,\"\n \" fv.verdict AS v\" + sql + _SEARCH_SORTS[sort],\n args).fetchall()\n if ring:\n rows = [r for r in rows if _point_in_poly(r[\"lng\"], r[\"lat\"], ring)]\n total = len(rows)\n\n # points compacts : [uid, lng, lat, prix, verdict] — l'index dans ce\n # tableau détermine la page (points et liste partagent le même tri)\n points = [[r[\"uid\"], round(r[\"lng\"], 6), round(r[\"lat\"], 6), r[\"price\"],\n _FV_CODES.get(r[\"v\"] or \"\", None)] for r in rows]\n\n # page demandée, ramenée dans les bornes si les filtres l'ont dépassée\n last = max(1, -(-total // page_size))\n page = min(max(1, page), last)\n page_uids = [r[\"uid\"] for r in rows[(page - 1) * page_size: page * page_size]]\n\n listings: list[dict] = []\n if page_uids:\n marks = \",\".join(\"?\" * len(page_uids))\n by_uid = {r[\"uid\"]: _row_to_dict(r) for r in con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence,\" + _KS_COLS +\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\" + _KS_JOIN +\n f\" WHERE uid IN ({marks})\", page_uids).fetchall()}\n listings = [by_uid[u] for u in page_uids if u in by_uid]\n\n # annonces filtrées mais sans coordonnées (affichage honnête, hors carte)\n sql_nogeo = sql.replace(\n \" AND listings.lat IS NOT NULL AND listings.lng IS NOT NULL\",\n \" AND (listings.lat IS NULL OR listings.lng IS NULL)\", 1)\n args_nogeo = list(args)\n for spatial in (bbox, poly):\n if spatial:\n sql_nogeo = sql_nogeo.replace(\n \" AND listings.lat BETWEEN ? AND ?\"\n \" AND listings.lng BETWEEN ? AND ?\", \"\", 1)\n del args_nogeo[0:4]\n unpositioned = con.execute(\"SELECT COUNT(*) c\" + sql_nogeo,\n args_nogeo).fetchone()[\"c\"]\n con.close()\n\n return {\n \"total\": total, \"page\": page, \"page_size\": page_size, \"sort\": sort,\n \"listings\": listings,\n \"points\": points if include != \"liste\" else None,\n \"unpositioned\": unpositioned,\n }\n\n\n# --- Miniatures optimisées (WebP, taille écran) ------------------------------\n# Sert les images des annonces redimensionnées et compressées (perf mobile) :\n# /api/img?u=&w=480. Seules les URLs déjà vérifiées par le contrôle\n# qualité (table image_checks) sont proxifiées — pas de proxy ouvert. Si\n# l'URL n'est pas (encore) connue : 404, le frontend retombe sur l'original.\n_IMG_WIDTHS = (160, 480, 800, 1280)\n_IMG_CACHE = ROOT / \"data\" / \"imgcache\"\n\n\n@app.get(\"/api/img\")\ndef img_thumbnail(u: str, w: int = 480):\n import hashlib\n import requests as _rq\n from fastapi.responses import Response\n w = min(_IMG_WIDTHS, key=lambda x: abs(x - w))\n if not u.startswith((\"http://\", \"https://\")):\n raise HTTPException(400, \"URL invalide\")\n key = hashlib.sha1(u.encode()).hexdigest()\n _IMG_CACHE.mkdir(parents=True, exist_ok=True)\n path = _IMG_CACHE / f\"{key}_{w}.webp\"\n headers = {\"Cache-Control\": \"public, max-age=2592000, immutable\"}\n if path.exists():\n return Response(path.read_bytes(), media_type=\"image/webp\",\n headers=headers)\n con = db.connect()\n known = con.execute(\"SELECT status FROM image_checks WHERE url=?\",\n (u,)).fetchone()\n con.close()\n if known is None or known[\"status\"] not in (\"ok\",):\n raise HTTPException(404, \"image inconnue du contrôle qualité\")\n try:\n resp = _rq.get(u, timeout=12, headers={\"User-Agent\": \"LouKaImg/1.0\"})\n resp.raise_for_status()\n from io import BytesIO\n from PIL import Image, ImageOps\n img = Image.open(BytesIO(resp.content))\n img = ImageOps.exif_transpose(img)\n if img.mode not in (\"RGB\", \"L\"):\n img = img.convert(\"RGB\")\n if img.width > w:\n img = img.resize((w, max(1, round(img.height * w / img.width))),\n Image.LANCZOS)\n buf = BytesIO()\n img.save(buf, \"WEBP\", quality=80, method=4)\n data = buf.getvalue()\n path.write_bytes(data)\n return Response(data, media_type=\"image/webp\", headers=headers)\n except HTTPException:\n raise\n except Exception:\n raise HTTPException(404, \"image irrécupérable\")\n\n\n@app.get(\"/api/listings/{uid}/pdf\")\ndef listing_pdf(uid: str):\n \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n from fastapi.responses import Response\n from . import pdfgen\n data = pdfgen.fiche_pdf(uid)\n if data is None:\n raise HTTPException(404, \"Annonce introuvable\")\n nom = uid.replace(\":\", \"-\")\n return Response(content=data, media_type=\"application/pdf\", headers={\n \"Content-Disposition\": f'attachment; filename=\"louka-fiche-{nom}.pdf\"'})\n\n\n@app.get(\"/api/stats/rapport.pdf\")\ndef rapport_marche_pdf():\n \"\"\"Rapport global du marché locatif (PDF multi-pages).\"\"\"\n from fastapi.responses import Response\n from . import pdfgen\n return Response(content=pdfgen.rapport_pdf(), media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n 'attachment; filename=\"louka-rapport-marche.pdf\"'})\n\n\n@app.get(\"/api/fairvalue/{uid}\")\ndef fairvalue_detail(uid: str):\n \"\"\"Analyse de prix Lou-Ka : juste valeur, fourchette, confiance,\n distribution du segment (voir louka/fairvalue.py).\"\"\"\n from . import fairvalue\n d = fairvalue.explain(uid)\n if d is None:\n raise HTTPException(404, \"Pas d'estimation pour cette annonce\")\n return d\n\n\n@app.get(\"/api/hydro\")\ndef hydro_at(adresse: str, solve: int = 0, uid: str | None = None,\n lat: float | None = None, lng: float | None = None):\n \"\"\"Estimation du coût d'électricité à une adresse (Hydro-Québec).\n Par défaut cache seulement (aucun captcha) ; solve=1 force le calcul.\n uid/lat/lng permettent la lecture cache par annonce et la dérivation du\n code postal (adresses sans CP).\"\"\"\n from . import hydro\n return hydro.estimate(adresse=adresse, uid=uid, lat=lat, lng=lng,\n solve=bool(solve))\n\n\n@app.get(\"/api/commerces\")\ndef commerces_at(lat: float, lng: float):\n \"\"\"Grands commerces + métro/bus les plus proches (Mapbox / OSM).\"\"\"\n from . import commerces\n return commerces.nearby(lat, lng)\n\n\n@app.get(\"/api/air\")\ndef air_at(lat: float, lng: float):\n \"\"\"Qualité de l'air : station RSQAQ la plus proche (MELCCFP).\"\"\"\n from . import air\n d = air.lookup(lat, lng)\n if d is None:\n raise HTTPException(404, \"Base qualité de l'air non disponible\")\n return d\n\n\n@app.get(\"/api/gaz\")\ndef gaz_at(lat: float, lng: float):\n \"\"\"Stations-service à proximité et prix courants (gazquebec.ca).\"\"\"\n from . import gaz\n return gaz.nearby(lat, lng)\n\n\n@app.get(\"/api/inondation\")\ndef inondation_at(lat: float, lng: float):\n \"\"\"Risque d'inondation à l'adresse (BDZI, gouv. du Québec) —\n bloc « Risque d'inondation » de la fiche.\"\"\"\n from . import inondation\n d = inondation.lookup(lat, lng)\n if d is None:\n raise HTTPException(404, \"Base des zones inondables non disponible\")\n return d\n\n\n@app.get(\"/api/rdl\")\ndef rdl_nearby(lat: float, lng: float, radius: int = 600):\n \"\"\"Loyers déclarés au Registre des loyers (registre-des-loyers.ca)\n autour d'un point — bloc « Registre des loyers » de la fiche.\"\"\"\n from . import rdl\n radius = max(100, min(radius, 2000))\n d = rdl.nearby(lat, lng, radius)\n if d is None:\n raise HTTPException(404, \"Registre des loyers non disponible\")\n return d\n\n\n@app.get(\"/api/tal\")\ndef tal_history(address: str, city: str | None = None):\n \"\"\"Historique au Tribunal administratif du logement (décisions SOQUIJ)\n à l'adresse de l'annonce — bloc « Historique TAL » de la fiche.\n Une adresse jamais vérifiée est mise en file prioritaire (status pending).\"\"\"\n from . import tal\n return tal.lookup(address, city)\n\n\n@app.get(\"/api/listings/{uid}/historique\")\ndef listing_history(uid: str):\n \"\"\"Historique Lou-Ka : timeline des observations réelles (prix, texte,\n photos, disponibilité, retraits/retours) — voir louka/history.py.\"\"\"\n from . import history\n out = history.timeline(uid)\n if out is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return out\n\n\n@app.get(\"/api/listings/{uid}/cout-reel\")\ndef listing_cout_reel(uid: str):\n \"\"\"Coût réel mensuel : loyer + frais non inclus ligne par ligne, chaque\n poste étiqueté included/observed/estimated/unknown — louka/coutreel.py.\"\"\"\n from . import coutreel\n out = coutreel.compute(uid)\n if out is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return out\n\n\n@app.get(\"/api/listings/{uid}/recyclees\")\ndef listing_recycled(uid: str):\n \"\"\"Annonces antérieures probables du même logement (score multi-signaux,\n inférence explicable) — louka/recycled.py.\"\"\"\n from . import recycled\n return recycled.lookup(uid)\n\n\n@app.get(\"/api/immeuble\")\ndef immeuble_fiche(bkey: str):\n \"\"\"Passeport de l'immeuble (stats précalculées par building.rollup).\"\"\"\n from . import building\n out = building.fiche(bkey)\n if out is None:\n raise HTTPException(404, \"Immeuble inconnu\")\n return out\n\n\n@app.get(\"/api/managers/{source_id}\")\ndef manager_fiche(source_id: str):\n \"\"\"Fiche du gestionnaire (source directe) + avis Google synchronisés en\n base — aucun appel SerpApi au chargement de page (louka/managers.py).\"\"\"\n from . import managers\n out = managers.fiche(source_id)\n if out is None:\n raise HTTPException(404, \"Gestionnaire inconnu\")\n return out\n\n\n@app.get(\"/api/listings/{uid}\")\ndef get_listing(uid: str):\n con = db.connect()\n row = con.execute(\n \"SELECT listings.*, fv.fv AS fv, fv.fv_low, fv.fv_high,\"\n \" fv.deviation AS fv_deviation, fv.verdict AS fv_verdict,\"\n \" fv.confidence AS fv_confidence\"\n \" FROM listings LEFT JOIN fairvalue fv USING (uid)\"\n \" WHERE uid=?\", (uid,)).fetchone()\n d = None\n if row is not None:\n d = _row_to_dict(row)\n # commodités de proximité (cache par immeuble, voir louka/poi.py)\n if d.get(\"lat\") is not None and d.get(\"lng\") is not None:\n key = f\"{round(d['lat'], 4)},{round(d['lng'], 4)}\"\n poi_row = con.execute(\n \"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n d[\"poi\"] = json.loads(poi_row[\"pois\"]) if poi_row else []\n else:\n d[\"poi\"] = []\n # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n if d.get(\"coord_key\"):\n ks = con.execute(\n \"SELECT walk, transit, bike, calme, services, global AS glob,\"\n \" details, version, computed_at FROM kascores WHERE coord_key=?\",\n (d[\"coord_key\"],)).fetchone()\n if ks is not None:\n d[\"kascores\"] = {\n \"walk\": ks[\"walk\"], \"transit\": ks[\"transit\"],\n \"bike\": ks[\"bike\"], \"calme\": ks[\"calme\"],\n \"services\": ks[\"services\"], \"global\": ks[\"glob\"],\n \"details\": json.loads(ks[\"details\"] or \"{}\"),\n \"version\": ks[\"version\"], \"computed_at\": ks[\"computed_at\"],\n }\n # statistiques de quartier (recensement, proximité, chaleur, criminalité)\n from . import quartier\n dauid = d.get(\"dauid\")\n d[\"quartier\"] = quartier.fiche_quartier(\n d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n # description structurée + historique de prix\n d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None\n d[\"price_history\"] = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n if d is None:\n raise HTTPException(404, \"Annonce introuvable\")\n return d\n\n\n@app.get(\"/api/kascores/stats\")\ndef kascores_stats():\n \"\"\"Couverture, moyennes et version du barème — page « KA Scores ».\"\"\"\n from . import kascores\n return kascores.stats()\n\n\n@app.get(\"/api/facets\")\ndef facets(city: str | None = None):\n \"\"\"Valeurs distinctes pour construire les filtres du frontend.\n\n `city` (optionnel) restreint la liste des quartiers à cette ville —\n utilisé par le sélecteur « Quartier » dépendant de « Ville ».\n \"\"\"\n con = db.connect()\n sector_sql = (\"SELECT DISTINCT sector FROM listings\"\n \" WHERE active=1 AND published=1 AND dup_of IS NULL\"\n \" AND sector<>''\")\n sector_args: list = []\n if city:\n sector_sql += \" AND city=?\"\n sector_args.append(city)\n out = {\n \"cities\": [r[\"city\"] for r in con.execute(\n \"SELECT DISTINCT city FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL AND city<>'' ORDER BY city\")],\n \"sectors\": [r[\"sector\"] for r in con.execute(\n sector_sql + \" ORDER BY sector\", sector_args)],\n \"unit_types\": [r[\"unit_type\"] for r in con.execute(\n \"SELECT DISTINCT unit_type FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL AND unit_type<>'' ORDER BY unit_type\")],\n \"sources\": [dict(r) for r in con.execute(\n \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n \" AND dup_of IS NULL GROUP BY source ORDER BY n DESC\")],\n }\n con.close()\n return out\n\n\n@app.get(\"/api/sources\")\ndef sources():\n registry = json.loads(SOURCES_PATH.read_text(encoding=\"utf-8\"))[\"sources\"]\n con = db.connect()\n counts = {r[\"source\"]: r[\"n\"] for r in con.execute(\n \"SELECT source, COUNT(*) n FROM listings WHERE active=1 AND published=1\"\n \" GROUP BY source\")}\n last = {r[\"source\"]: r[\"ts\"] for r in con.execute(\n \"SELECT source, MAX(ts) ts FROM sync_log WHERE ok=1 GROUP BY source\")}\n con.close()\n for s in registry:\n s[\"active_listings\"] = counts.get(s[\"id\"], 0)\n s[\"last_sync\"] = last.get(s[\"id\"])\n return {\"sources\": registry}\n\n\n# Villes de la grande région de Montréal (le reste de la province est\n# compté dans « autres » — Outaouais, Estrie, Mauricie, Est-du-Québec…)\n_VILLES_GM = (\n \"Montréal\", \"Montréal-Est\", \"Laval\", \"Longueuil\", \"Brossard\", \"Terrebonne\",\n \"Mascouche\", \"Repentigny\", \"Boucherville\", \"Saint-Lambert\", \"Westmount\",\n \"Côte Saint-Luc\", \"Dorval\", \"Pointe-Claire\", \"Verdun\", \"LaSalle\", \"Anjou\",\n \"Saint-Constant\", \"Delson\", \"Candiac\", \"Châteauguay\", \"Chambly\",\n \"Sainte-Julie\", \"Varennes\", \"Belœil\", \"McMasterville\", \"Mont-Saint-Hilaire\",\n \"Saint-Basile-le-Grand\", \"Saint-Bruno-de-Montarville\", \"Boisbriand\",\n \"Richelieu\", \"Beauharnois\", \"Salaberry-de-Valleyfield\", \"Vaudreuil-Dorion\",\n \"Kirkland\", \"Beaconsfield\", \"Dollard-des-Ormeaux\", \"Mont-Royal\", \"Outremont\",\n)\n\n\n@app.get(\"/api/stats\")\ndef stats(syncs_since_h: float | None = None):\n \"\"\"Stats globales + journal de synchronisation.\n\n `syncs_since_h` (optionnel) : fenêtre en heures pour `recent_syncs` — le\n moniteur central (api-ka) polle aux 2 h et les 20 dernières entrées ne\n couvrent que ~11 min d'activité sur ~200 sources (fausses alertes\n « stale »). Avec le paramètre : toutes les entrées des N dernières heures\n (max 4000) ; s'il y en a moins de 20, on retombe sur les 20 dernières\n toutes époques confondues. Sans le paramètre : comportement historique\n inchangé (LIMIT 20).\n \"\"\"\n con = db.connect()\n gm = \",\".join(\"?\" * len(_VILLES_GM))\n row = con.execute(\n f\"\"\"SELECT COUNT(*) total,\n SUM(CASE WHEN city='Québec' THEN 1 ELSE 0 END) quebec,\n SUM(CASE WHEN city='Lévis' THEN 1 ELSE 0 END) levis,\n SUM(CASE WHEN city IN ({gm}) THEN 1 ELSE 0 END) montreal,\n SUM(CASE WHEN city NOT IN ('Québec','Lévis') AND city NOT IN ({gm})\n THEN 1 ELSE 0 END) autres,\n COUNT(DISTINCT source) sources,\n AVG(price) avg_price\n FROM listings WHERE active=1 AND published=1 AND dup_of IS NULL\"\"\",\n list(_VILLES_GM) + list(_VILLES_GM)).fetchone()\n log: list[dict] = []\n if syncs_since_h is not None:\n import time as _time\n cutoff = _time.time() - syncs_since_h * 3600\n log = [dict(r) for r in con.execute(\n \"SELECT * FROM sync_log WHERE ts >= ? ORDER BY ts DESC LIMIT 4000\",\n (cutoff,))]\n if syncs_since_h is None or len(log) < 20:\n log = [dict(r) for r in con.execute(\n \"SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20\")]\n # qualité des données : complétude moyenne + quarantaine (quality.py)\n qual = con.execute(\n \"\"\"SELECT ROUND(AVG(completeness),1) avg_completeness,\n SUM(published=1) publiees, SUM(published=0) quarantaine\n FROM listings WHERE active=1\"\"\"\n ).fetchone()\n con.close()\n return {**dict(row), \"quality\": dict(qual), \"recent_syncs\": log}\n\n\n@app.get(\"/api/stats/detailed\")\ndef stats_detailed():\n \"\"\"Agrégats du marché (source unique : louka/marketstats.py).\"\"\"\n from . import marketstats\n return marketstats.compute()\n\n\n# --- Module Stats commun Groupe KA (contrat frontend/src/ka/stats/SPEC.md) ---\n_PERIODES_VALIDES = {\"auj\", \"7j\", \"30j\", \"3m\", \"6m\", \"12m\", \"annee\", \"tout\"}\n\n\n@app.get(\"/api/stats/dashboard\")\ndef stats_dashboard(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None):\n \"\"\"Tableau de bord analytique (KPI, séries, répartitions, tableaux…).\"\"\"\n from . import statsdash\n if period not in _PERIODES_VALIDES and not (from_ and to):\n period = \"30j\"\n return statsdash.compute(period, from_, to)\n\n\n@app.get(\"/api/stats/report\")\ndef stats_report(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None,\n mode: str = \"complet\"):\n \"\"\"Rapport statistique PDF estampillé Groupe-KA — 5 modes :\n complet | synthese | tendances | repartitions | donnees (inconnu → complet).\"\"\"\n from fastapi.responses import Response\n from . import kapdf, statsdash\n if period not in _PERIODES_VALIDES:\n period = \"perso\" if (from_ and to) else \"30j\"\n if mode not in kapdf.REPORT_MODES:\n mode = \"complet\"\n dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n from_, to)\n eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n data = kapdf.GroupeKAReport(site=site, dashboard=dash, mode=mode).build()\n fname = kapdf.filename(\"lou-ka\", period, mode)\n return Response(content=data, media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n f'attachment; filename=\"{fname}\"'})\n\n\n\n@app.get(\"/api/stats/catalog\")\ndef stats_catalog(period: str = \"30j\",\n from_: str | None = Query(None, alias=\"from\"),\n to: str | None = None):\n \"\"\"v3 — blocs composables pour le constructeur de rapports personnalisés.\"\"\"\n from . import kapdf, statsdash\n if period not in _PERIODES_VALIDES and not (from_ and to):\n period = \"30j\"\n dash = statsdash.compute(period if period in _PERIODES_VALIDES else \"30j\",\n from_, to)\n return {\"updated\": dash.get(\"updated\"), \"period\": dash.get(\"period\"),\n \"blocks\": kapdf.catalog(dash)}\n\n\n@app.post(\"/api/stats/report/custom\")\ndef stats_report_custom(spec: dict = Body(...)):\n \"\"\"v3 — rapport PDF personnalisé : {\"title\", \"period\", \"from\", \"to\",\n \"blocks\": [{\"key\": \"series:…\", \"render\": \"bar\"}, …]} (SPEC.md §3bis).\"\"\"\n from fastapi.responses import Response\n from . import kapdf, statsdash\n period = str(spec.get(\"period\") or \"30j\")\n from_ = str(spec.get(\"from\") or \"\") or None\n to = str(spec.get(\"to\") or \"\") or None\n if period not in _PERIODES_VALIDES:\n period = \"perso\" if (from_ and to) else \"30j\"\n dash = statsdash.compute(period if period != \"perso\" else \"30j\",\n from_, to)\n known = {b[\"key\"] for b in kapdf.catalog(dash)}\n blocks = [b for b in (spec.get(\"blocks\") or [])\n if isinstance(b, dict) and b.get(\"key\") in known][:40]\n if not blocks:\n raise HTTPException(400, \"Aucun bloc valide dans la composition\")\n eco = json.loads((ROOT / \"frontend\" / \"src\" / \"ka\" /\n \"ecosystem.json\").read_text(encoding=\"utf-8\"))\n site = next(s for s in eco[\"sites\"] if s[\"id\"] == \"lou-ka\")\n data = kapdf.GroupeKAReport(\n site=site, dashboard=dash, mode=kapdf.CUSTOM_MODE,\n spec={\"title\": str(spec.get(\"title\") or \"\")[:80], \"blocks\": blocks},\n ).build()\n fname = kapdf.filename(\"lou-ka\", period, kapdf.CUSTOM_MODE)\n return Response(content=data, media_type=\"application/pdf\",\n headers={\"Content-Disposition\":\n f'attachment; filename=\"{fname}\"'})\n\n\n@app.post(\"/api/sync\")\ndef trigger_sync(background: BackgroundTasks, source: str | None = None):\n \"\"\"Déclenche une synchronisation (équivalent d'un webhook entrant).\"\"\"\n def _job():\n with _sync_lock:\n ingest.run([source] if source else None)\n background.add_task(_job)\n return {\"status\": \"démarré\", \"source\": source or \"toutes\"}\n\n\n# --- Frontend React (build Vite) --------------------------------------------\nif FRONTEND_DIST.exists():\n app.mount(\"/assets\", StaticFiles(directory=FRONTEND_DIST / \"assets\"), name=\"assets\")\n\n # référencement : SSR des routes publiques, robots.txt, sitemaps, 410.\n # Inclus AVANT le catch-all — l'ordre d'enregistrement fait foi.\n from . import seo # noqa: E402\n app.include_router(seo.router)\n\n # Documentation utilisateur (frontend/public/doc → dist/doc) — routes\n # explicites AVANT le catch-all SPA pour servir /doc/ comme un index.\n @app.get(\"/doc\")\n def doc_redirect():\n return RedirectResponse(\"/doc/\", status_code=308)\n\n @app.get(\"/doc/\")\n def doc_index():\n return FileResponse(FRONTEND_DIST / \"doc\" / \"index.html\")\n\n @app.middleware(\"http\")\n async def _cache_headers(request, call_next):\n \"\"\"Politique de cache : les bundles hachés (/assets/…) sont immuables,\n mais index.html doit TOUJOURS être revalidé — sinon les navigateurs\n gardent une vieille version de l'app après un déploiement.\"\"\"\n resp = await call_next(request)\n path = request.url.path\n if path.startswith(\"/assets/\"):\n resp.headers[\"Cache-Control\"] = \"public, max-age=31536000, immutable\"\n elif \"text/html\" in (resp.headers.get(\"content-type\") or \"\"):\n resp.headers[\"Cache-Control\"] = \"no-cache\"\n return resp\n\n # routes de l'app rendues uniquement côté client (privées ou volatiles) —\n # tout autre chemin inconnu renvoie index.html avec un statut 404 : le\n # routeur React affiche sa page « introuvable », les moteurs de recherche\n # voient un vrai 404 (fin des soft-404).\n _CLIENT_ROUTES = {\"profil\", \"favoris\", \"gestion\", \"bienvenue\", \"bot\",\n \"contact\", \"juste-valeur\", \"court-terme\"}\n _CLIENT_PREFIXES = (\"u/\", \"passerelle/\", \"court-terme/\")\n\n @app.get(\"/{full_path:path}\")\n def spa(full_path: str):\n target = FRONTEND_DIST / full_path\n if full_path and target.is_file():\n return FileResponse(target)\n known = (full_path in _CLIENT_ROUTES\n or any(full_path == p.rstrip(\"/\") or full_path.startswith(p)\n for p in _CLIENT_PREFIXES))\n return FileResponse(FRONTEND_DIST / \"index.html\",\n status_code=200 if known else 404,\n headers={\"Cache-Control\": \"no-cache\"})\n","structuredPatch":[{"oldStart":701,"oldLines":6,"newStart":701,"newLines":14,"lines":[" d[\"quartier\"] = quartier.fiche_quartier("," d.get(\"lat\"), d.get(\"lng\"), d.get(\"city\") or \"\","," dauid if dauid and dauid != \"hors-zone\" else None)","+ # passeport de l'immeuble (stats précalculées) + score hiver","+ if d.get(\"building_key\"):","+ from . import building","+ d[\"immeuble\"] = building.fiche(d[\"building_key\"], con)","+ else:","+ d[\"immeuble\"] = None","+ from . import hiver","+ d[\"hiver\"] = hiver.score(d.get(\"lat\"), d.get(\"lng\"), con)"," # description structurée + historique de prix"," d[\"digest\"] = json.loads(d[\"digest\"]) if d.get(\"digest\") else None"," d[\"price_history\"] = [dict(r) for r in con.execute("]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"fc1b0b82-3816-40be-ae03-7ea03265ec95","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLLwURLfwrSKgWzTgnE","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS6AMKpwEIERgCKkAf6eUuVnnqO4w/g+4uevnOKyhNuMQSVpcCyPiV8g1/xL91NvdcTSjLey6P5LvQ0ntGQZ1TQGHost6F0F43y9U+Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGOgbPUBrABAhIM/NQ97Ue3P8x/CnW0GgxZTtthjDA49tW5DagiMCBgMVD1kQR04NnOslVhFK6Co+mnKA9jqydJbAkqipagT9n42cCxY+bNXgVZh6YzCSrtATZK7/AO1Hbd6Cb9DfZYf6fm6lb0zG388jTts0uEjB++vFITBfOJ7uLdNWxkmnz4Ues4ruW+u7WVJ1xwU4y/0hZYRAtPYSrFqcgWNMImRXMiHMpRiwzwETQ+DD7QxgfVvaGGp1l6j/Js003hjOaFA+0bgl7TXz8HdueAQ3lMMKNkYca6T45qu5h+hB3q6vAX53LnfyMfVGsZOLQXZu/hleNP9XkrmWDj4SnbbUwbmqgDqofE4gTCjfohAfDOj3sZIM7/YocVp82+GNOil+kdmTnbTiI8UdP4PLgOiFQqZy04DY8ky64GdU/8jNHizRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":403,"cache_read_input_tokens":116197,"cache_creation":{"ephemeral_5m_input_tokens":403,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a1fbb18a-6fd9-430a-9d51-ad6952993cf9"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLLwURLfwrSKgWzTgnE","type":"message","role":"assistant","content":[{"type":"text","text":"Maintenant le CLI run.py et les hooks dans ingest.watch()."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":403,"cache_read_input_tokens":116197,"cache_creation":{"ephemeral_5m_input_tokens":403,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"22a6e401-ca4e-41aa-82a0-5413f0706903"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLLwURLfwrSKgWzTgnE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01D3hrYtowvKZSGQB1DavoS3","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/run.py","offset":88,"limit":80},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":403,"cache_read_input_tokens":116197,"cache_creation":{"ephemeral_5m_input_tokens":403,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5644e884-7317-45eb-aeb5-fab63da13fa6"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01D3hrYtowvKZSGQB1DavoS3","type":"tool_result","content":"88\t limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n89\t print(environment.run(limit))\n90\t elif cmd == \"kascores\":\n91\t # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n92\t # incrémental ; « python run.py kascores all » force tout le parc\n93\t from louka import kascores\n94\t force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n95\t print(kascores.run(recompute_all=force))\n96\t elif cmd == \"inondation-build\":\n97\t from louka import inondation\n98\t inondation.build()\n99\t elif cmd == \"air-refresh\":\n100\t from louka import air\n101\t air.refresh()\n102\t elif cmd == \"gaz-refresh\":\n103\t from louka import gaz\n104\t print(f\"[gaz] {gaz.refresh()} stations\")\n105\t elif cmd == \"hydro-precompute\":\n106\t from louka import hydro\n107\t n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n108\t b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n109\t hydro.precompute(limit=n, budget=b)\n110\t elif cmd == \"rdl\":\n111\t from louka import rdl\n112\t rdl.refresh()\n113\t elif cmd == \"tal\":\n114\t # historique TAL (décisions SOQUIJ) par adresse, par lots\n115\t from louka import tal\n116\t n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n117\t b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n118\t tal.precompute(limit=n, budget=b)\n119\t elif cmd == \"record\":\n120\t from louka import fixtures\n121\t from louka.connectors import CONNECTORS\n122\t targets = sys.argv[2:] or sorted(CONNECTORS)\n123\t for sid in targets:\n124\t try:\n125\t print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n126\t except Exception as exc:\n127\t print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n128\t elif cmd == \"serve\":\n129\t import uvicorn\n130\t port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n131\t uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n132\t else:\n133\t print(__doc__)\n134\t sys.exit(1)\n135\t\n136\t\n137\tif __name__ == \"__main__\":\n138\t main()\n139\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bd77d122-0537-4b01-8683-68724333cdd8","timestamp":"2026-08-24T22:07:11.692Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/run.py","content":" limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n print(environment.run(limit))\n elif cmd == \"kascores\":\n # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n # incrémental ; « python run.py kascores all » force tout le parc\n from louka import kascores\n force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n print(kascores.run(recompute_all=force))\n elif cmd == \"inondation-build\":\n from louka import inondation\n inondation.build()\n elif cmd == \"air-refresh\":\n from louka import air\n air.refresh()\n elif cmd == \"gaz-refresh\":\n from louka import gaz\n print(f\"[gaz] {gaz.refresh()} stations\")\n elif cmd == \"hydro-precompute\":\n from louka import hydro\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n hydro.precompute(limit=n, budget=b)\n elif cmd == \"rdl\":\n from louka import rdl\n rdl.refresh()\n elif cmd == \"tal\":\n # historique TAL (décisions SOQUIJ) par adresse, par lots\n from louka import tal\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n tal.precompute(limit=n, budget=b)\n elif cmd == \"record\":\n from louka import fixtures\n from louka.connectors import CONNECTORS\n targets = sys.argv[2:] or sorted(CONNECTORS)\n for sid in targets:\n try:\n print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n except Exception as exc:\n print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n elif cmd == \"serve\":\n import uvicorn\n port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n else:\n print(__doc__)\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","numLines":52,"startLine":88,"totalLines":139}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLLwURLfwrSKgWzTgnE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KCosM8ufGu4Yd9eme5Bpqh","name":"Grep","input":{"pattern":"def watch|dedup|fairvalue|kascores|hydro|tal|rollup","path":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","output_mode":"content","-n":true},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":403,"cache_read_input_tokens":116197,"cache_creation":{"ephemeral_5m_input_tokens":403,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"90fc4345-f1e7-4e82-83d9-c7077840fb80"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KCosM8ufGu4Yd9eme5Bpqh","type":"tool_result","content":"91:def watch(interval_seconds: int = 3600) -> None:\n122: from . import dedup\n123: stats = dedup.run()\n124: print(f\"[lou-ka] dedup: {stats}\")\n126: print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n127: try: # juste valeur locative (fair value) — après dedup/geocode/quality\n128: from . import fairvalue\n129: fairvalue.compute_all()\n131: print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n134: from . import environment, kascores\n136: stats_ks = kascores.run()\n137: print(f\"[lou-ka] kascores: {stats_ks}\")\n139: print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n141: from . import hydro\n142: hydro.precompute(limit=100, budget=30)\n144: print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n147: from . import tal\n148: tal.precompute(limit=60, budget=600)\n150: print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"53f43c85-2eb9-4840-a601-50f3803988df","timestamp":"2026-08-24T22:07:13.173Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"91:def watch(interval_seconds: int = 3600) -> None:\n122: from . import dedup\n123: stats = dedup.run()\n124: print(f\"[lou-ka] dedup: {stats}\")\n126: print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n127: try: # juste valeur locative (fair value) — après dedup/geocode/quality\n128: from . import fairvalue\n129: fairvalue.compute_all()\n131: print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n134: from . import environment, kascores\n136: stats_ks = kascores.run()\n137: print(f\"[lou-ka] kascores: {stats_ks}\")\n139: print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n141: from . import hydro\n142: hydro.precompute(limit=100, budget=30)\n144: print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n147: from . import tal\n148: tal.precompute(limit=60, budget=600)\n150: print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)","numLines":19}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"5319604b-dcd0-4e91-b0db-b0dd62f3f63c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLMTqHy55wsc4vh3Q9F","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JJohztNxheKQpxtNe4YW7A","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","offset":115,"limit":50},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1859,"cache_read_input_tokens":116600,"cache_creation":{"ephemeral_5m_input_tokens":1859,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"79285d15-4a83-44e0-9fc3-2a59a01b831b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JJohztNxheKQpxtNe4YW7A","type":"tool_result","content":"115\t print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n116\t try: # contrôle qualité des images, par lots incrémentaux\n117\t from . import imgcheck\n118\t imgcheck.run(1500)\n119\t except Exception as exc:\n120\t print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n121\t try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n122\t from . import dedup\n123\t stats = dedup.run()\n124\t print(f\"[lou-ka] dedup: {stats}\")\n125\t except Exception as exc:\n126\t print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n127\t try: # juste valeur locative (fair value) — après dedup/geocode/quality\n128\t from . import fairvalue\n129\t fairvalue.compute_all()\n130\t except Exception as exc:\n131\t print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n132\t try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n133\t # incrémentaux des nouveaux immeubles — après géocodage\n134\t from . import environment, kascores\n135\t environment.run(limit=4)\n136\t stats_ks = kascores.run()\n137\t print(f\"[lou-ka] kascores: {stats_ks}\")\n138\t except Exception as exc:\n139\t print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n140\t try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n141\t from . import hydro\n142\t hydro.precompute(limit=100, budget=30)\n143\t except Exception as exc:\n144\t print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n145\t try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n146\t # puis backfill des adresses jamais vérifiées (coût borné)\n147\t from . import tal\n148\t tal.precompute(limit=60, budget=600)\n149\t except Exception as exc:\n150\t print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n151\t print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n152\t time.sleep(interval_seconds)\n153\t\n154\t\n155\tif __name__ == \"__main__\":\n156\t run(sys.argv[1:] or None)\n157\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"771d74e0-26db-4691-9d58-d3a3abf055d2","timestamp":"2026-08-24T22:07:18.870Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","content":" print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # contrôle qualité des images, par lots incrémentaux\n from . import imgcheck\n imgcheck.run(1500)\n except Exception as exc:\n print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n from . import dedup\n stats = dedup.run()\n print(f\"[lou-ka] dedup: {stats}\")\n except Exception as exc:\n print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n # incrémentaux des nouveaux immeubles — après géocodage\n from . import environment, kascores\n environment.run(limit=4)\n stats_ks = kascores.run()\n print(f\"[lou-ka] kascores: {stats_ks}\")\n except Exception as exc:\n print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n from . import hydro\n hydro.precompute(limit=100, budget=30)\n except Exception as exc:\n print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n time.sleep(interval_seconds)\n\n\nif __name__ == \"__main__\":\n run(sys.argv[1:] or None)\n","numLines":43,"startLine":115,"totalLines":157}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6d995c22-d419-4e5e-bc81-535861e9b690","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLMswNceS6PQuX98kX9","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01L6LWqCKqRppQSYagVTo7wJ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","old_string":" try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)","new_string":" try: # passeport des immeubles — après dedup (utilise dup_of + adresse)\n from . import building\n stats_b = building.rollup()\n print(f\"[lou-ka] immeubles: {stats_b}\")\n except Exception as exc:\n print(f\"[lou-ka] immeubles: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1079,"cache_read_input_tokens":118459,"cache_creation":{"ephemeral_5m_input_tokens":1079,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"efda7ed2-4079-4eb9-907c-0d8e471e7555"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01L6LWqCKqRppQSYagVTo7wJ","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3ad177d4-5592-481f-a02d-f2f896c05717","timestamp":"2026-08-24T22:07:26.225Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","oldString":" try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)","newString":" try: # passeport des immeubles — après dedup (utilise dup_of + adresse)\n from . import building\n stats_b = building.rollup()\n print(f\"[lou-ka] immeubles: {stats_b}\")\n except Exception as exc:\n print(f\"[lou-ka] immeubles: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# ingest.py : pipeline d'ingestion — exécute les connecteurs et synchronise\n# la base (ajouts / mises à jour / retraits) = contenu toujours à jour\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport sys\nimport time\nimport traceback\n\nfrom . import db\nfrom .connectors import CONNECTORS\n\n# Disjoncteur (circuit breaker) : une source qui a échoué aux BREAKER_FAILS\n# derniers passages consécutifs est SAUTÉE (les timeouts/retries des sources\n# mortes ralentissaient tout le cycle), puis retentée un passage sur\n# BREAKER_RETRY_EVERY. Les sauts sont consignés dans sync_log (ok=0,\n# message « disjoncteur: … ») — ils comptent donc comme échecs consécutifs,\n# ce qui fait avancer le compteur jusqu'à la prochaine retentative.\nBREAKER_FAILS = 5\nBREAKER_RETRY_EVERY = 5\n\n\ndef _consecutive_failures(con, source: str) -> int:\n \"\"\"Nombre d'entrées sync_log ok=0 consécutives (les plus récentes).\"\"\"\n rows = con.execute(\n \"SELECT ok FROM sync_log WHERE source=? ORDER BY ts DESC LIMIT 60\",\n (source,)).fetchall()\n n = 0\n for r in rows:\n if r[\"ok\"]:\n break\n n += 1\n return n\n\n\ndef run(sources: list[str] | None = None) -> list[dict]:\n \"\"\"Exécute l'ingestion pour toutes les sources (ou celles demandées).\"\"\"\n con = db.connect()\n results = []\n explicit = sources is not None # sync ciblé : ignorer le disjoncteur\n targets = sources or list(CONNECTORS.keys())\n for sid in targets:\n cls = CONNECTORS.get(sid)\n if cls is None:\n print(f\"[lou-ka] connecteur inconnu : {sid}\", file=sys.stderr)\n continue\n if not explicit:\n fails = _consecutive_failures(con, sid)\n if fails >= BREAKER_FAILS and fails % BREAKER_RETRY_EVERY != 0:\n print(f\"[lou-ka] {sid} sauté (disjoncteur : \"\n f\"{fails} échecs consécutifs)\")\n db.log_failure(con, sid,\n f\"disjoncteur: sauté ({fails} échecs consécutifs)\")\n results.append({\"source\": sid, \"skipped\": True, \"fails\": fails})\n continue\n t0 = time.time()\n print(f\"[lou-ka] sync {sid} ...\")\n try:\n listings = cls().fetch()\n finalized, dropped = [], 0\n for lst in listings:\n try:\n finalized.append(lst.finalize())\n except Exception: # une annonce malformée ne bloque pas la source\n dropped += 1\n stats = db.sync_source(con, sid, finalized)\n stats[\"seconds\"] = round(time.time() - t0, 1)\n if dropped:\n stats[\"dropped\"] = dropped\n if stats.get(\"alert\"):\n print(f\"[lou-ka] ⚠ ALERTE {sid} : {stats['alert']}\")\n print(f\"[lou-ka] {stats}\")\n results.append(stats)\n except Exception as exc: # robustesse : une source ne bloque pas les autres\n db.log_failure(con, sid, f\"{exc}\")\n traceback.print_exc()\n results.append({\"source\": sid, \"error\": str(exc)})\n # statistiques du planificateur SQLite : sans elles, les requêtes bbox de\n # la carte (Ka Maps) retombent sur idx_listings_active au lieu du géo-index\n try:\n con.execute(\"PRAGMA optimize\")\n except Exception:\n pass\n con.close()\n return results\n\n\ndef watch(interval_seconds: int = 3600) -> None:\n \"\"\"Boucle de rafraîchissement périodique (pseudo-webhook par sondage).\"\"\"\n while True:\n run()\n try: # géocoder les nouvelles adresses EN LOT (Adresses Québec)\n from . import geocode\n geocode.run_batch()\n except Exception as exc:\n print(f\"[lou-ka] geocode: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # commodités de proximité des nouveaux immeubles (cache aussi)\n from . import poi\n poi.run(limit=80)\n except Exception as exc:\n print(f\"[lou-ka] poi: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # aire de diffusion (stats de quartier) des nouvelles annonces\n from . import quartier\n quartier.enrich()\n except Exception as exc:\n print(f\"[lou-ka] quartier: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # rafraîchir le score de complétude (le géocodage vient de compléter\n # des coordonnées) + publication/quarantaine\n from . import quality\n quality.backfill()\n except Exception as exc:\n print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # contrôle qualité des images, par lots incrémentaux\n from . import imgcheck\n imgcheck.run(1500)\n except Exception as exc:\n print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n from . import dedup\n stats = dedup.run()\n print(f\"[lou-ka] dedup: {stats}\")\n except Exception as exc:\n print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n # incrémentaux des nouveaux immeubles — après géocodage\n from . import environment, kascores\n environment.run(limit=4)\n stats_ks = kascores.run()\n print(f\"[lou-ka] kascores: {stats_ks}\")\n except Exception as exc:\n print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n from . import hydro\n hydro.precompute(limit=100, budget=30)\n except Exception as exc:\n print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n time.sleep(interval_seconds)\n\n\nif __name__ == \"__main__\":\n run(sys.argv[1:] or None)\n","structuredPatch":[{"oldStart":124,"oldLines":6,"newStart":124,"newLines":12,"lines":[" print(f\"[lou-ka] dedup: {stats}\")"," except Exception as exc:"," print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)","+ try: # passeport des immeubles — après dedup (utilise dup_of + adresse)","+ from . import building","+ stats_b = building.rollup()","+ print(f\"[lou-ka] immeubles: {stats_b}\")","+ except Exception as exc:","+ print(f\"[lou-ka] immeubles: erreur non bloquante: {exc}\", file=sys.stderr)"," try: # juste valeur locative (fair value) — après dedup/geocode/quality"," from . import fairvalue"," fairvalue.compute_all()"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"a8dfb182-b707-4b3c-92ed-89f6f77749e1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLNR2egwrEDeAxq75Nw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FF4czRP6a7u5WBjLdXGcPz","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","old_string":" try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)","new_string":" try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # gestionnaires : résolution Google Maps + avis (SerpApi,\n # serveur seulement, budget borné ; sauté sans SERPAPI_API_KEY)\n from . import managers\n stats_m = managers.precompute(budget_s=120)\n print(f\"[lou-ka] gestionnaires: {stats_m}\")\n except Exception as exc:\n print(f\"[lou-ka] gestionnaires: erreur non bloquante: {exc}\",\n file=sys.stderr)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1090,"cache_read_input_tokens":119538,"cache_creation":{"ephemeral_5m_input_tokens":1090,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"dcf261c4-d0b1-4552-b4a3-89eb70599bdc"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FF4czRP6a7u5WBjLdXGcPz","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"158ff569-0eb6-400e-8ec4-5a706747d0fd","timestamp":"2026-08-24T22:07:35.628Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/ingest.py","oldString":" try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)","newString":" try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # gestionnaires : résolution Google Maps + avis (SerpApi,\n # serveur seulement, budget borné ; sauté sans SERPAPI_API_KEY)\n from . import managers\n stats_m = managers.precompute(budget_s=120)\n print(f\"[lou-ka] gestionnaires: {stats_m}\")\n except Exception as exc:\n print(f\"[lou-ka] gestionnaires: erreur non bloquante: {exc}\",\n file=sys.stderr)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# ingest.py : pipeline d'ingestion — exécute les connecteurs et synchronise\n# la base (ajouts / mises à jour / retraits) = contenu toujours à jour\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport sys\nimport time\nimport traceback\n\nfrom . import db\nfrom .connectors import CONNECTORS\n\n# Disjoncteur (circuit breaker) : une source qui a échoué aux BREAKER_FAILS\n# derniers passages consécutifs est SAUTÉE (les timeouts/retries des sources\n# mortes ralentissaient tout le cycle), puis retentée un passage sur\n# BREAKER_RETRY_EVERY. Les sauts sont consignés dans sync_log (ok=0,\n# message « disjoncteur: … ») — ils comptent donc comme échecs consécutifs,\n# ce qui fait avancer le compteur jusqu'à la prochaine retentative.\nBREAKER_FAILS = 5\nBREAKER_RETRY_EVERY = 5\n\n\ndef _consecutive_failures(con, source: str) -> int:\n \"\"\"Nombre d'entrées sync_log ok=0 consécutives (les plus récentes).\"\"\"\n rows = con.execute(\n \"SELECT ok FROM sync_log WHERE source=? ORDER BY ts DESC LIMIT 60\",\n (source,)).fetchall()\n n = 0\n for r in rows:\n if r[\"ok\"]:\n break\n n += 1\n return n\n\n\ndef run(sources: list[str] | None = None) -> list[dict]:\n \"\"\"Exécute l'ingestion pour toutes les sources (ou celles demandées).\"\"\"\n con = db.connect()\n results = []\n explicit = sources is not None # sync ciblé : ignorer le disjoncteur\n targets = sources or list(CONNECTORS.keys())\n for sid in targets:\n cls = CONNECTORS.get(sid)\n if cls is None:\n print(f\"[lou-ka] connecteur inconnu : {sid}\", file=sys.stderr)\n continue\n if not explicit:\n fails = _consecutive_failures(con, sid)\n if fails >= BREAKER_FAILS and fails % BREAKER_RETRY_EVERY != 0:\n print(f\"[lou-ka] {sid} sauté (disjoncteur : \"\n f\"{fails} échecs consécutifs)\")\n db.log_failure(con, sid,\n f\"disjoncteur: sauté ({fails} échecs consécutifs)\")\n results.append({\"source\": sid, \"skipped\": True, \"fails\": fails})\n continue\n t0 = time.time()\n print(f\"[lou-ka] sync {sid} ...\")\n try:\n listings = cls().fetch()\n finalized, dropped = [], 0\n for lst in listings:\n try:\n finalized.append(lst.finalize())\n except Exception: # une annonce malformée ne bloque pas la source\n dropped += 1\n stats = db.sync_source(con, sid, finalized)\n stats[\"seconds\"] = round(time.time() - t0, 1)\n if dropped:\n stats[\"dropped\"] = dropped\n if stats.get(\"alert\"):\n print(f\"[lou-ka] ⚠ ALERTE {sid} : {stats['alert']}\")\n print(f\"[lou-ka] {stats}\")\n results.append(stats)\n except Exception as exc: # robustesse : une source ne bloque pas les autres\n db.log_failure(con, sid, f\"{exc}\")\n traceback.print_exc()\n results.append({\"source\": sid, \"error\": str(exc)})\n # statistiques du planificateur SQLite : sans elles, les requêtes bbox de\n # la carte (Ka Maps) retombent sur idx_listings_active au lieu du géo-index\n try:\n con.execute(\"PRAGMA optimize\")\n except Exception:\n pass\n con.close()\n return results\n\n\ndef watch(interval_seconds: int = 3600) -> None:\n \"\"\"Boucle de rafraîchissement périodique (pseudo-webhook par sondage).\"\"\"\n while True:\n run()\n try: # géocoder les nouvelles adresses EN LOT (Adresses Québec)\n from . import geocode\n geocode.run_batch()\n except Exception as exc:\n print(f\"[lou-ka] geocode: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # commodités de proximité des nouveaux immeubles (cache aussi)\n from . import poi\n poi.run(limit=80)\n except Exception as exc:\n print(f\"[lou-ka] poi: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # aire de diffusion (stats de quartier) des nouvelles annonces\n from . import quartier\n quartier.enrich()\n except Exception as exc:\n print(f\"[lou-ka] quartier: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # rafraîchir le score de complétude (le géocodage vient de compléter\n # des coordonnées) + publication/quarantaine\n from . import quality\n quality.backfill()\n except Exception as exc:\n print(f\"[lou-ka] quality: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # contrôle qualité des images, par lots incrémentaux\n from . import imgcheck\n imgcheck.run(1500)\n except Exception as exc:\n print(f\"[lou-ka] imgcheck: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # déduplication inter-sources (APRÈS géocodage : coords requises)\n from . import dedup\n stats = dedup.run()\n print(f\"[lou-ka] dedup: {stats}\")\n except Exception as exc:\n print(f\"[lou-ka] dedup: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # passeport des immeubles — après dedup (utilise dup_of + adresse)\n from . import building\n stats_b = building.rollup()\n print(f\"[lou-ka] immeubles: {stats_b}\")\n except Exception as exc:\n print(f\"[lou-ka] immeubles: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # juste valeur locative (fair value) — après dedup/geocode/quality\n from . import fairvalue\n fairvalue.compute_all()\n except Exception as exc:\n print(f\"[lou-ka] fairvalue: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # environnement OSM (nouvelles tuiles seulement) + KA Scores\n # incrémentaux des nouveaux immeubles — après géocodage\n from . import environment, kascores\n environment.run(limit=4)\n stats_ks = kascores.run()\n print(f\"[lou-ka] kascores: {stats_ks}\")\n except Exception as exc:\n print(f\"[lou-ka] kascores: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # estimation Hydro-Québec des nouvelles annonces (cache persistant, coût borné)\n from . import hydro\n hydro.precompute(limit=100, budget=30)\n except Exception as exc:\n print(f\"[lou-ka] hydro: erreur non bloquante: {exc}\", file=sys.stderr)\n try: # historique TAL (SOQUIJ) par adresse — file des fiches d'abord,\n # puis backfill des adresses jamais vérifiées (coût borné)\n from . import tal\n tal.precompute(limit=60, budget=600)\n except Exception as exc:\n print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)\n print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")\n time.sleep(interval_seconds)\n\n\nif __name__ == \"__main__\":\n run(sys.argv[1:] or None)\n","structuredPatch":[{"oldStart":154,"oldLines":6,"newStart":154,"newLines":14,"lines":[" tal.precompute(limit=60, budget=600)"," except Exception as exc:"," print(f\"[lou-ka] tal: erreur non bloquante: {exc}\", file=sys.stderr)","+ try: # gestionnaires : résolution Google Maps + avis (SerpApi,","+ # serveur seulement, budget borné ; sauté sans SERPAPI_API_KEY)","+ from . import managers","+ stats_m = managers.precompute(budget_s=120)","+ print(f\"[lou-ka] gestionnaires: {stats_m}\")","+ except Exception as exc:","+ print(f\"[lou-ka] gestionnaires: erreur non bloquante: {exc}\",","+ file=sys.stderr)"," print(f\"[lou-ka] prochaine synchronisation dans {interval_seconds}s\")"," time.sleep(interval_seconds)"," "]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"92092f03-71a4-4f89-811f-f75d44e76f34","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLP7E7FEZwdrCJoAsYw","type":"message","role":"assistant","content":[{"type":"text","text":"Ajout des commandes CLI dans run.py."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":639,"cache_read_input_tokens":120628,"cache_creation":{"ephemeral_5m_input_tokens":639,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2948eebf-9ff0-48ff-8d2f-10ecd1f816e0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLP7E7FEZwdrCJoAsYw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MiLvinR898UZsYSqgn4q8R","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/run.py","old_string":" elif cmd == \"record\":","new_string":" elif cmd == \"buildings\":\n # (re)calcule le passeport de tous les immeubles (building_key + stats)\n from louka import building\n print(building.rollup())\n elif cmd == \"managers\":\n # gestionnaires : amorçage + résolution Google Maps + avis (SerpApi)\n # « python run.py managers [budget_s] »\n from louka import managers\n b = float(sys.argv[2]) if len(sys.argv) > 2 else 300\n print(managers.precompute(budget_s=b))\n elif cmd == \"record\":"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":639,"cache_read_input_tokens":120628,"cache_creation":{"ephemeral_5m_input_tokens":639,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3756742a-a11a-4323-b56a-056dbe8cddf5"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MiLvinR898UZsYSqgn4q8R","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/run.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"45850cf4-2534-4982-9314-df0683dcede1","timestamp":"2026-08-24T22:07:41.502Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/run.py","oldString":" elif cmd == \"record\":","newString":" elif cmd == \"buildings\":\n # (re)calcule le passeport de tous les immeubles (building_key + stats)\n from louka import building\n print(building.rollup())\n elif cmd == \"managers\":\n # gestionnaires : amorçage + résolution Google Maps + avis (SerpApi)\n # « python run.py managers [budget_s] »\n from louka import managers\n b = float(sys.argv[2]) if len(sys.argv) > 2 else 300\n print(managers.precompute(budget_s=b))\n elif cmd == \"record\":","originalFile":"#!/usr/bin/env python3\n# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# run.py : point d'entrée — `sync`, `watch`, `serve`\n# -----------------------------------------------------------------------------\n\"\"\"Utilisation :\n python run.py sync [source ...] # synchronise les annonces\n python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n python run.py sync-ct [source ...] # synchronise la section court terme\n python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n python run.py record # enregistre les fixtures de test d'une source\n python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom pathlib import Path\n\n# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n_env = Path(__file__).parent / \".env\"\nif _env.exists():\n for line in _env.read_text().splitlines():\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n k, _, v = line.partition(\"=\")\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef main() -> None:\n cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n if cmd == \"sync\":\n from louka import ingest\n ingest.run(sys.argv[2:] or None)\n elif cmd == \"watch\":\n from louka import ingest\n minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n ingest.watch(minutes * 60)\n elif cmd == \"sync-ct\":\n from louka.shortterm import ingest as ct_ingest\n ct_ingest.run(sys.argv[2:] or None)\n elif cmd == \"watch-ct\":\n from louka.shortterm import ingest as ct_ingest\n minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n ct_ingest.watch(minutes * 60)\n elif cmd == \"geocode1\":\n from louka import geocode\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n geocode.run(limit)\n elif cmd == \"geocode\":\n # lot Adresses Québec (rapide) ; « geocode1 » = ancien mode 1-par-1\n from louka import geocode\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n geocode.run_batch(limit)\n elif cmd == \"quality\":\n # (re)calcule le score de complétude + publication/quarantaine\n from louka import quality\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n quality.backfill(limit)\n elif cmd == \"imgaudit\":\n # contrôle qualité des images (liens morts, minuscules, placeholders,\n # doublons) — incrémental : python run.py imgaudit [n_annonces] [source]\n from louka import imgcheck\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n src = sys.argv[3] if len(sys.argv) > 3 else None\n imgcheck.run(limit, source=src)\n elif cmd == \"fairvalue\":\n # (re)calcule la juste valeur locative de toutes les annonces publiées\n from louka import fairvalue\n fairvalue.compute_all()\n elif cmd == \"poi\":\n from louka import poi\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n poi.run(limit)\n elif cmd == \"quartier\":\n from louka import quartier\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n quartier.enrich(limit)\n elif cmd == \"env\":\n # données d'environnement OSM par tuile (routes, rails, cyclable,\n # bars, POI complets) — cache ~3 mois, socle des KA Scores\n from louka import environment\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n print(environment.run(limit))\n elif cmd == \"kascores\":\n # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n # incrémental ; « python run.py kascores all » force tout le parc\n from louka import kascores\n force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n print(kascores.run(recompute_all=force))\n elif cmd == \"inondation-build\":\n from louka import inondation\n inondation.build()\n elif cmd == \"air-refresh\":\n from louka import air\n air.refresh()\n elif cmd == \"gaz-refresh\":\n from louka import gaz\n print(f\"[gaz] {gaz.refresh()} stations\")\n elif cmd == \"hydro-precompute\":\n from louka import hydro\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n hydro.precompute(limit=n, budget=b)\n elif cmd == \"rdl\":\n from louka import rdl\n rdl.refresh()\n elif cmd == \"tal\":\n # historique TAL (décisions SOQUIJ) par adresse, par lots\n from louka import tal\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n tal.precompute(limit=n, budget=b)\n elif cmd == \"record\":\n from louka import fixtures\n from louka.connectors import CONNECTORS\n targets = sys.argv[2:] or sorted(CONNECTORS)\n for sid in targets:\n try:\n print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n except Exception as exc:\n print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n elif cmd == \"serve\":\n import uvicorn\n port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n else:\n print(__doc__)\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","structuredPatch":[{"oldStart":116,"oldLines":6,"newStart":116,"newLines":16,"lines":[" n = int(sys.argv[2]) if len(sys.argv) > 2 else 60"," b = int(sys.argv[3]) if len(sys.argv) > 3 else 600"," tal.precompute(limit=n, budget=b)","+ elif cmd == \"buildings\":","+ # (re)calcule le passeport de tous les immeubles (building_key + stats)","+ from louka import building","+ print(building.rollup())","+ elif cmd == \"managers\":","+ # gestionnaires : amorçage + résolution Google Maps + avis (SerpApi)","+ # « python run.py managers [budget_s] »","+ from louka import managers","+ b = float(sys.argv[2]) if len(sys.argv) > 2 else 300","+ print(managers.precompute(budget_s=b))"," elif cmd == \"record\":"," from louka import fixtures"," from louka.connectors import CONNECTORS"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"576df266-2bed-4b07-9898-287204f45fb1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLPYbqbBbCdc6YTFBJs","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS8AIKpwEIERgCKkD8eIcFRS7EsrZykQha4U9bZbkjOYmgqusGv2Vj73JXeHghHTALv4t0g9wO12OUXB+HXleLZhMryzo66CVLHoBBMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGxgbPUBrABAhIMira+EXxPKtkSmoEoGgyt1X3gD0hWICNKFCwiMBF2qd5pRYj83iv36t6e82v6S5lnT/09VcUX5LWVTBdzN68F5HNBbXmgVQ6uvPsbISp2H8lyHLlambC5gfDA4hVJdCLJtHubCpmQPBon52EpW/alORK4Nzf2w1s7lTLxs+IaXxCxsPCXJpk+3QJ20P1J18Qf99pZ5h7Vd6iIs5GaqYq7mM5PB6x8V2hJ5PDh3RTZtKCMJtl8Bmr/aI7KfghEz5GWWvPpXRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":399,"cache_read_input_tokens":121267,"cache_creation":{"ephemeral_5m_input_tokens":399,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2cc34398-9783-4923-a581-c48b4a37f246"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLPYbqbBbCdc6YTFBJs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019W2bUqfCTEMihpr1Roe3oS","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/run.py","limit":34},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":399,"cache_read_input_tokens":121267,"cache_creation":{"ephemeral_5m_input_tokens":399,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d502be81-a94f-44be-bf60-d5b71fb4d2d0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019W2bUqfCTEMihpr1Roe3oS","type":"tool_result","content":"1\t#!/usr/bin/env python3\n2\t# -----------------------------------------------------------------------------\n3\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n4\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n5\t# run.py : point d'entrée — `sync`, `watch`, `serve`\n6\t# -----------------------------------------------------------------------------\n7\t\"\"\"Utilisation :\n8\t python run.py sync [source ...] # synchronise les annonces\n9\t python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n10\t python run.py sync-ct [source ...] # synchronise la section court terme\n11\t python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n12\t python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n13\t python run.py record # enregistre les fixtures de test d'une source\n14\t python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n15\t python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n16\t python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n17\t python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n18\t\"\"\"\n19\tfrom __future__ import annotations\n20\t\n21\timport os\n22\timport sys\n23\tfrom pathlib import Path\n24\t\n25\t# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n26\t_env = Path(__file__).parent / \".env\"\n27\tif _env.exists():\n28\t for line in _env.read_text().splitlines():\n29\t line = line.strip()\n30\t if line and not line.startswith(\"#\") and \"=\" in line:\n31\t k, _, v = line.partition(\"=\")\n32\t os.environ.setdefault(k.strip(), v.strip())\n33\t\n34\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3de6310b-b04f-4db3-b465-2c9fc70e1934","timestamp":"2026-08-24T22:07:45.854Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/run.py","content":"#!/usr/bin/env python3\n# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# run.py : point d'entrée — `sync`, `watch`, `serve`\n# -----------------------------------------------------------------------------\n\"\"\"Utilisation :\n python run.py sync [source ...] # synchronise les annonces\n python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n python run.py sync-ct [source ...] # synchronise la section court terme\n python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n python run.py record # enregistre les fixtures de test d'une source\n python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom pathlib import Path\n\n# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n_env = Path(__file__).parent / \".env\"\nif _env.exists():\n for line in _env.read_text().splitlines():\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n k, _, v = line.partition(\"=\")\n os.environ.setdefault(k.strip(), v.strip())\n\n","numLines":34,"startLine":1,"totalLines":149}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8553c82e-b770-49d2-8ed3-abc649666cc9","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLPs5awPE7qQnQuzpWg","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UtAyyMCsvTydMokFc7BGY6","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/run.py","old_string":" python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n\"\"\"","new_string":" python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n python run.py buildings # passeport des immeubles (building_key + stats)\n python run.py managers [budget_s] # gestionnaires : fiche Google Maps + avis (SerpApi)\n\"\"\""},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":840,"cache_read_input_tokens":121666,"cache_creation":{"ephemeral_5m_input_tokens":840,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"81756e8f-7294-47fa-8f40-a86051c146ba"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UtAyyMCsvTydMokFc7BGY6","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/run.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1e75de25-7703-4b08-ba97-b22474af1f43","timestamp":"2026-08-24T22:07:51.512Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/run.py","oldString":" python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n\"\"\"","newString":" python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n python run.py buildings # passeport des immeubles (building_key + stats)\n python run.py managers [budget_s] # gestionnaires : fiche Google Maps + avis (SerpApi)\n\"\"\"","originalFile":"#!/usr/bin/env python3\n# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# run.py : point d'entrée — `sync`, `watch`, `serve`\n# -----------------------------------------------------------------------------\n\"\"\"Utilisation :\n python run.py sync [source ...] # synchronise les annonces\n python run.py watch [minutes] # synchronise en boucle (défaut 60 min)\n python run.py sync-ct [source ...] # synchronise la section court terme\n python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)\n python run.py serve [port] # démarre l'API + le frontend (défaut 8080)\n python run.py record # enregistre les fixtures de test d'une source\n python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)\n python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)\n python run.py env [n] # environnement OSM par tuile (socle des KA Scores)\n python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom pathlib import Path\n\n# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe\n_env = Path(__file__).parent / \".env\"\nif _env.exists():\n for line in _env.read_text().splitlines():\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n k, _, v = line.partition(\"=\")\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef main() -> None:\n cmd = sys.argv[1] if len(sys.argv) > 1 else \"serve\"\n if cmd == \"sync\":\n from louka import ingest\n ingest.run(sys.argv[2:] or None)\n elif cmd == \"watch\":\n from louka import ingest\n minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n ingest.watch(minutes * 60)\n elif cmd == \"sync-ct\":\n from louka.shortterm import ingest as ct_ingest\n ct_ingest.run(sys.argv[2:] or None)\n elif cmd == \"watch-ct\":\n from louka.shortterm import ingest as ct_ingest\n minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360\n ct_ingest.watch(minutes * 60)\n elif cmd == \"geocode1\":\n from louka import geocode\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n geocode.run(limit)\n elif cmd == \"geocode\":\n # lot Adresses Québec (rapide) ; « geocode1 » = ancien mode 1-par-1\n from louka import geocode\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n geocode.run_batch(limit)\n elif cmd == \"quality\":\n # (re)calcule le score de complétude + publication/quarantaine\n from louka import quality\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n quality.backfill(limit)\n elif cmd == \"imgaudit\":\n # contrôle qualité des images (liens morts, minuscules, placeholders,\n # doublons) — incrémental : python run.py imgaudit [n_annonces] [source]\n from louka import imgcheck\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500\n src = sys.argv[3] if len(sys.argv) > 3 else None\n imgcheck.run(limit, source=src)\n elif cmd == \"fairvalue\":\n # (re)calcule la juste valeur locative de toutes les annonces publiées\n from louka import fairvalue\n fairvalue.compute_all()\n elif cmd == \"poi\":\n from louka import poi\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n poi.run(limit)\n elif cmd == \"quartier\":\n from louka import quartier\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n quartier.enrich(limit)\n elif cmd == \"env\":\n # données d'environnement OSM par tuile (routes, rails, cyclable,\n # bars, POI complets) — cache ~3 mois, socle des KA Scores\n from louka import environment\n limit = int(sys.argv[2]) if len(sys.argv) > 2 else None\n print(environment.run(limit))\n elif cmd == \"kascores\":\n # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)\n # incrémental ; « python run.py kascores all » force tout le parc\n from louka import kascores\n force = len(sys.argv) > 2 and sys.argv[2] == \"all\"\n print(kascores.run(recompute_all=force))\n elif cmd == \"inondation-build\":\n from louka import inondation\n inondation.build()\n elif cmd == \"air-refresh\":\n from louka import air\n air.refresh()\n elif cmd == \"gaz-refresh\":\n from louka import gaz\n print(f\"[gaz] {gaz.refresh()} stations\")\n elif cmd == \"hydro-precompute\":\n from louka import hydro\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 100\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 40\n hydro.precompute(limit=n, budget=b)\n elif cmd == \"rdl\":\n from louka import rdl\n rdl.refresh()\n elif cmd == \"tal\":\n # historique TAL (décisions SOQUIJ) par adresse, par lots\n from louka import tal\n n = int(sys.argv[2]) if len(sys.argv) > 2 else 60\n b = int(sys.argv[3]) if len(sys.argv) > 3 else 600\n tal.precompute(limit=n, budget=b)\n elif cmd == \"buildings\":\n # (re)calcule le passeport de tous les immeubles (building_key + stats)\n from louka import building\n print(building.rollup())\n elif cmd == \"managers\":\n # gestionnaires : amorçage + résolution Google Maps + avis (SerpApi)\n # « python run.py managers [budget_s] »\n from louka import managers\n b = float(sys.argv[2]) if len(sys.argv) > 2 else 300\n print(managers.precompute(budget_s=b))\n elif cmd == \"record\":\n from louka import fixtures\n from louka.connectors import CONNECTORS\n targets = sys.argv[2:] or sorted(CONNECTORS)\n for sid in targets:\n try:\n print(f\"[lou-ka] record {sid} ... {fixtures.record(sid)}\")\n except Exception as exc:\n print(f\"[lou-ka] record {sid} ÉCHEC : {exc}\", file=sys.stderr)\n elif cmd == \"serve\":\n import uvicorn\n port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080\n uvicorn.run(\"louka.web:app\", host=\"0.0.0.0\", port=port)\n else:\n print(__doc__)\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","structuredPatch":[{"oldStart":15,"oldLines":6,"newStart":15,"newLines":8,"lines":[" python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)"," python run.py env [n] # environnement OSM par tuile (socle des KA Scores)"," python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)","+ python run.py buildings # passeport des immeubles (building_key + stats)","+ python run.py managers [budget_s] # gestionnaires : fiche Google Maps + avis (SerpApi)"," \"\"\""," from __future__ import annotations"," "]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f17253c3-1f9b-4ea0-9722-91e9c07dbc08","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLQHH7y4aJd6rPepC8x","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS2gQKpwEIERgCKkDBkd/i8XWiI4ymbPIRmrEVRlqWZSdBS0urHBQsojwqixxY0C8MoSNQ7EVcRVbrfpoZ3/+hF42OH2aYTgAmbZP0Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG9gbPUBrABAhIM0duaWzSs74koIeP3Ggy8qy+qFoyMS4ep7R4iMMNh0sL0nFgUIvnQF7QEmmfAAnNmUUND2mdsLfRgDDPfwV+D+H70sziqJjgm7IDiuirfAqj1DN3IGOvdx/q33KDnHSADP26vyqYRh3CxtFZ7oiGQIkY/guJNpJZyIlBzXMwFpkVoJdfctTqHPr7O0GgGECv6lKkaqEogB59EFHyaYuIomfONV3CEvtoWih0CF2UhbXR9mw/6mMo9/2pI4ma/CUmDAJtOmhwtCZV8Q3PsYPjSXcj76Zi+sVwvMlOqJDweMKg9qVMHOlCpzP4BBSa50iuBASYa/NIeNQTHUE0ZsXgxDxy+LioyFH9TB1ZO/rkXdMekgJK9OJmpsVKK8/900fI5cKySg5TWzz8n1alMsKVz4CnV24GUEhJzdkE0YlzK0OrsxDLzW50RcBbSY87ZFkMzS2QBLCBSTe2G3pN7TOhliEMPSGJloJf1AVdnSPfr1wcKkeyp0igi9pLQAvF4mbhFdhtH31Lo3Ra31Blk5y48T6GOXLxTZUznyJzWLb9xAY2ku7cOE3jxd0SQZy746xgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":327,"cache_read_input_tokens":122506,"cache_creation":{"ephemeral_5m_input_tokens":327,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"811d51ec-a422-4f5b-9303-7304f4ebc673"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLQHH7y4aJd6rPepC8x","type":"message","role":"assistant","content":[{"type":"text","text":"run.py rollup initial des immeubles (peut prendre un moment sur ~100k+ annonces, actives + historiques)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":327,"cache_read_input_tokens":122506,"cache_creation":{"ephemeral_5m_input_tokens":327,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"90afe156-ecfe-4139-aa20-207226f0d4d7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLQHH7y4aJd6rPepC8x","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01S79vV868cCzfr1hf5nVxKY","name":"Bash","input":{"command":"set -a; source .env; set +a; time .venv/bin/python run.py buildings","description":"Initial building.rollup() on full DB","timeout":600000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":327,"cache_read_input_tokens":122506,"cache_creation":{"ephemeral_5m_input_tokens":327,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3ea8a825-f96f-46f3-bca4-3b90c310d937"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bxcwnerrr","tool_use_id":"toolu_01S79vV868cCzfr1hf5nVxKY","description":"Initial building.rollup() on full DB","task_type":"local_bash","uuid":"a3232846-a214-4070-84e3-3224f141c448","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bxcwnerrr","tool_use_id":"toolu_01S79vV868cCzfr1hf5nVxKY","status":"completed","output_file":"","summary":"Initial building.rollup() on full DB","uuid":"798da6d4-3d2a-43e7-b358-5c22663841f4","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01S79vV868cCzfr1hf5nVxKY","type":"tool_result","content":"{'buildings': 31874, 'listings_scanned': 97976, 'keys_updated': 78375}\n.venv/bin/python run.py buildings 1.91s user 1.89s system 93% cpu 4.091 total","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bf511a55-44ff-4e7c-a50c-3ea9661d5467","timestamp":"2026-08-24T22:08:02.428Z","tool_use_result":{"stdout":"{'buildings': 31874, 'listings_scanned': 97976, 'keys_updated': 78375}\n.venv/bin/python run.py buildings 1.91s user 1.89s system 93% cpu 4.091 total","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c43d178c-002f-4edb-857b-0a8bf6be3b44","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLR5pq5xur6w8gXYBFn","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS6QMKpwEIERgCKkA4b8xGZSXa4LnEZ87116fO/udqxtaet/wFKPH3xJU3gH1EoOYmCgpVivIk0wsK6Sqk9ddn/1sknWJ10i3DAIeyMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHHgbPUBrABAhIMXmO+StKI5G5Sx1HSGgwTWf0v428JDpEE7EMiMAYDRupPAmBRD2ysoqxJCM6FIiwSgH7Quly1HQalhoZpwDLsGyejsFkVgXscTG/Q+SruAZW3iq0MjTigIaYKrMSX5050iWjf0VV4vAoxbrCkO51DdN2n+NwMNdbFRopXoXYSHB6t+GsyNeQ3/fMICcWDFvG3b/Q6qubwvflxeuChlffbiamp28fuo+H7+rABcvxtsr0lugEMfEWAMTPWiYlW0QEFBvzg3YY05naY2xcsYmsfdHskVSWgxWJaIqfwHtX/AsfO5ZbQtTuL2KIAWrWTGpl1I/S8JvadJRCwLuqTSk8CpIOpb4Cr7P7DRHXLyUQ3RZabXR/3dLr+1/LrSlfoJoIUx21NdRIqNXAxyDoBhkSOBy8duooPmdipuf6o+E0YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":377,"cache_read_input_tokens":122833,"cache_creation":{"ephemeral_5m_input_tokens":377,"ephemeral_1h_input_tokens":0},"output_tokens":9,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"127ba32a-e5e3-43d2-88d9-ffac06a12642"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLR5pq5xur6w8gXYBFn","type":"message","role":"assistant","content":[{"type":"text","text":"31 874 immeubles en 4 s. Tests de sanité des modules sur des annonces réelles."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":377,"cache_read_input_tokens":122833,"cache_creation":{"ephemeral_5m_input_tokens":377,"ephemeral_1h_input_tokens":0},"output_tokens":9,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"88169a00-fb29-4401-84a3-4076e92dd7b7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLR5pq5xur6w8gXYBFn","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0167A55AZn4EcKRGp8bcciSC","name":"Bash","input":{"command":"set -a; source .env; set +a; .venv/bin/python -c \"\nimport json\nfrom louka import db, history, coutreel, recycled, building, hiver\ncon = db.connect()\n# une annonce active géocodée avec prix+superficie dans un immeuble à historique\nr = con.execute('''SELECT l.uid, l.building_key FROM listings l JOIN buildings b ON b.bkey=l.building_key\n WHERE l.active=1 AND l.published=1 AND l.dup_of IS NULL AND l.price IS NOT NULL\n AND l.area_sqft>100 AND l.lat IS NOT NULL\n AND json_extract(b.stats,'\\$.annonces_total') > 10 LIMIT 1''').fetchone()\nuid, bkey = r['uid'], r['building_key']\nprint('uid:', uid, '| bkey:', bkey)\nh = history.timeline(uid, con); print('historique:', {k: h[k] for k in ('jours_en_ligne','prix_initial','prix_actuel','variation','modifications')}, '| items:', len(h['timeline']))\nc = coutreel.compute(uid, con); print('cout reel total:', c['total_estime'], '| postes inconnus:', c['postes_inconnus'], '| pi2:', c['pi2'])\nrec = recycled.lookup(uid, con); print('recyclees:', len(rec['matches']), rec['matches'][:1])\nb = building.fiche(bkey, con); print('immeuble:', {k: b.get(k) for k in ('annonces_total','annonces_actives','unites_estimees','loyer_median','rotation','pression_loyers')})\nrow = con.execute('SELECT lat,lng FROM listings WHERE uid=?', (uid,)).fetchone()\nhv = hiver.score(row['lat'], row['lng'], con); print('hiver:', hv and {k: hv[k] for k in ('score','classe')}, hv and [(d['critere'], d['score']) for d in hv['detail']])\ncon.close()\n\"","description":"Sanity-test all new modules on real listing","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":377,"cache_read_input_tokens":122833,"cache_creation":{"ephemeral_5m_input_tokens":377,"ephemeral_1h_input_tokens":0},"output_tokens":9,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fd683333-6ec6-43a5-9d2a-fb019cbe5c7e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0167A55AZn4EcKRGp8bcciSC","type":"tool_result","content":"uid: app_urbains:archipel-217 | bkey: adr:7070|guillaume-couture|levis\nhistorique: {'jours_en_ligne': 18, 'prix_initial': None, 'prix_actuel': 1380.0, 'variation': None, 'modifications': 0} | items: 0\ncout reel total: 1440 | postes inconnus: ['Électricité', 'Chauffage', 'Eau chaude'] | pi2: {'valeur': 1.7, 'statut': 'calculated', 'percentile_ville': 6, 'n_ville': 406, 'portee_ville': 'Lévis, 1 ch.', 'percentile_secteur': 7, 'n_secteur': 95}\nrecyclees: 0 []\nimmeuble: {'annonces_total': 14, 'annonces_actives': 9, 'unites_estimees': 9, 'loyer_median': 1650, 'rotation': {'statut': 'donnees_insuffisantes'}, 'pression_loyers': None}\nhiver: {'score': 41, 'classe': 'exigeant'} [('Épicerie', 2), ('Pharmacie', 59), ('Arrêt de bus', 100), ('Dépanneur', 12)]","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b1ffa196-836c-4c22-9e20-357e7a0e0113","timestamp":"2026-08-24T22:08:15.317Z","tool_use_result":{"stdout":"uid: app_urbains:archipel-217 | bkey: adr:7070|guillaume-couture|levis\nhistorique: {'jours_en_ligne': 18, 'prix_initial': None, 'prix_actuel': 1380.0, 'variation': None, 'modifications': 0} | items: 0\ncout reel total: 1440 | postes inconnus: ['Électricité', 'Chauffage', 'Eau chaude'] | pi2: {'valeur': 1.7, 'statut': 'calculated', 'percentile_ville': 6, 'n_ville': 406, 'portee_ville': 'Lévis, 1 ch.', 'percentile_secteur': 7, 'n_secteur': 95}\nrecyclees: 0 []\nimmeuble: {'annonces_total': 14, 'annonces_actives': 9, 'unites_estimees': 9, 'loyer_median': 1650, 'rotation': {'statut': 'donnees_insuffisantes'}, 'pression_loyers': None}\nhiver: {'score': 41, 'classe': 'exigeant'} [('Épicerie', 2), ('Pharmacie', 59), ('Arrêt de bus', 100), ('Dépanneur', 12)]","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"21abe633-25cc-49ea-b2c3-f9c72622f329","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLS2sWEqZ1UbCurMwZ8","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISsQUKpwEIERgCKkAZBq86Xr7e8yCTkn1y6tuUAJWfgWE+LgdiLhQf49y0xZYgjk10+ekiro93dlWGjK/2gP8eC7ZuwEf5lkfd0X6mMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHVgbPUBrABAhIMArfZb64W5uTNRRlnGgxz8TCzcXnt7Uy9eFIiMDqLQ7He4CRW5+BG6UTRjpWAmeBEgkS+RSOJ19zYV6aOAkLgIYQjdnNY1u/4plrTUiq2A1mYxLlPqbhZKuJsEEIbw8HQT1PudJI6Ad66xHisRMSTek2mAuYk0ac2avG2WdOg/RR6+/3HMgOmkETBJxDbCf6aFwgIg8UzPRos1fgBoaXRadglX/q2kyei7vgeqGj5D3G+oynadXlhYm54/Ze5/QaTYMeL03YpV7f6EsOaRdinpfbP6es9vf/P3Yeu37iKN86QmnxNaxKtr3VIORSSbSXDKpSeVIIF6GLFst83NZ1lpmqrYWIF1XzwzE4jEtVNpbLoRh0KrOGehaVSD/hD/kKgFPKuEp6L+Ro9K/Q2q8VHmIeJZeZOL0S8Wa9dGjCejGWprJnJQ7RQKPUCdhmWS+BOPW+t4Y7l7aUYVFOJ1Ru8K1uYHQZJ6gHjqJommv2777QnbiV03/DSRvuBWQ4C9uR6aVdJjaQz4mlrJRNYFtb8Ze5tIGRa5FcZaiJKX+Rrcyu7oLs7EV1dwW2S/jiDiX+q61ScBMOJqSVS+RCeP3eyJkHYkupgGkLFhU6vTe1wRicuIVdCLvSOZD7yTolvJMz8HrxojjOCX54U8blFIYMmycjY//2LN0Gy1HEeVwloV7xLLznhARgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1328,"cache_read_input_tokens":123210,"cache_creation":{"ephemeral_5m_input_tokens":1328,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9e230347-bda0-4f11-bfd9-d06d9e6dea71"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLS2sWEqZ1UbCurMwZ8","type":"message","role":"assistant","content":[{"type":"text","text":"Tout fonctionne. Un détail : `prix_initial` est None faute d'entrée price_log — je vérifie si price_log enregistre le premier prix."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1328,"cache_read_input_tokens":123210,"cache_creation":{"ephemeral_5m_input_tokens":1328,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"57ec56cd-7fda-41ac-8fda-637da7ac936a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLS2sWEqZ1UbCurMwZ8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WfyatTgFyhnhd3dQzAnBGT","name":"Grep","input":{"pattern":"price_log","path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","output_mode":"content","-C":6,"head_limit":60},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1328,"cache_read_input_tokens":123210,"cache_creation":{"ephemeral_5m_input_tokens":1328,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3e439651-429e-4259-b510-90cc59de85b8"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WfyatTgFyhnhd3dQzAnBGT","type":"tool_result","content":"85- key TEXT, -- hash du contenu « liste » de l'annonce\n86- payload TEXT, -- JSON opaque propre au connecteur\n87- fetched_at REAL,\n88- PRIMARY KEY (source, external_id)\n89-);\n90-\n91:CREATE TABLE IF NOT EXISTS price_log (\n92- uid TEXT NOT NULL,\n93- ts REAL NOT NULL,\n94- price REAL -- prix observé (NULL = retiré de l'affichage)\n95-);\n96:CREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n97-\n98-CREATE TABLE IF NOT EXISTS poi_cache (\n99- coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n100- lat REAL,\n101- lng REAL,\n102- pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n--\n178-CREATE TABLE IF NOT EXISTS listing_events (\n179- id INTEGER PRIMARY KEY AUTOINCREMENT,\n180- uid TEXT NOT NULL, -- listings.uid\n181- ts REAL NOT NULL,\n182- event TEXT NOT NULL, -- description | superficie | dispo |\n183- -- inclusions | photos | disparition |\n184: -- reapparition (prix -> price_log)\n185- old TEXT, -- valeur avant (JSON compact)\n186- new TEXT -- valeur après (JSON compact)\n187-);\n188-CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n189-\n190-CREATE TABLE IF NOT EXISTS buildings (\n--\n358-\n359-\n360-def _log_events(con: sqlite3.Connection, uid: str, ts: float,\n361- old: sqlite3.Row, new: dict) -> None:\n362- \"\"\"Journalise les changements observés d'une annonce (listing_events).\n363-\n364: Le prix a déjà son historique dédié (price_log) — ici : description,\n365- superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n366- texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n367- \"\"\"\n368- ev: list[tuple[str, str | None, str | None]] = []\n369- old_desc = (old[\"description\"] or \"\").strip()\n370- new_desc = (new.get(\"description\") or \"\").strip()\n--\n477- :price,:price_label,:availability,\n478- :availability_date,:area_sqft,:pets,:furnished,\n479- :description,:digest,:amenities,:details,:images,:lat,:lng,\n480- :content_hash,:now,:now,:now,0,1,\n481- :completeness,:published,:quality_reasons)\"\"\", params)\n482- if lst.price is not None: # prix initial = point de départ de l'historique\n483: con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n484- (lst.uid, now, lst.price))\n485- added += 1\n486- elif row[\"content_hash\"] != h:\n487- # COALESCE : ne jamais écraser des coordonnées géocodées par null\n488- con.execute(\n489- \"\"\"UPDATE listings SET url=:url, title=:title,\n\n[Showing results with pagination = limit: 60]"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ef12e594-1d77-4d49-9838-a031c952e993","timestamp":"2026-08-24T22:08:22.871Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"85- key TEXT, -- hash du contenu « liste » de l'annonce\n86- payload TEXT, -- JSON opaque propre au connecteur\n87- fetched_at REAL,\n88- PRIMARY KEY (source, external_id)\n89-);\n90-\n91:CREATE TABLE IF NOT EXISTS price_log (\n92- uid TEXT NOT NULL,\n93- ts REAL NOT NULL,\n94- price REAL -- prix observé (NULL = retiré de l'affichage)\n95-);\n96:CREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n97-\n98-CREATE TABLE IF NOT EXISTS poi_cache (\n99- coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n100- lat REAL,\n101- lng REAL,\n102- pois TEXT, -- JSON : [{cat, name, dist_m}] (plus proche/catégorie)\n--\n178-CREATE TABLE IF NOT EXISTS listing_events (\n179- id INTEGER PRIMARY KEY AUTOINCREMENT,\n180- uid TEXT NOT NULL, -- listings.uid\n181- ts REAL NOT NULL,\n182- event TEXT NOT NULL, -- description | superficie | dispo |\n183- -- inclusions | photos | disparition |\n184: -- reapparition (prix -> price_log)\n185- old TEXT, -- valeur avant (JSON compact)\n186- new TEXT -- valeur après (JSON compact)\n187-);\n188-CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n189-\n190-CREATE TABLE IF NOT EXISTS buildings (\n--\n358-\n359-\n360-def _log_events(con: sqlite3.Connection, uid: str, ts: float,\n361- old: sqlite3.Row, new: dict) -> None:\n362- \"\"\"Journalise les changements observés d'une annonce (listing_events).\n363-\n364: Le prix a déjà son historique dédié (price_log) — ici : description,\n365- superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n366- texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n367- \"\"\"\n368- ev: list[tuple[str, str | None, str | None]] = []\n369- old_desc = (old[\"description\"] or \"\").strip()\n370- new_desc = (new.get(\"description\") or \"\").strip()\n--\n477- :price,:price_label,:availability,\n478- :availability_date,:area_sqft,:pets,:furnished,\n479- :description,:digest,:amenities,:details,:images,:lat,:lng,\n480- :content_hash,:now,:now,:now,0,1,\n481- :completeness,:published,:quality_reasons)\"\"\", params)\n482- if lst.price is not None: # prix initial = point de départ de l'historique\n483: con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n484- (lst.uid, now, lst.price))\n485- added += 1\n486- elif row[\"content_hash\"] != h:\n487- # COALESCE : ne jamais écraser des coordonnées géocodées par null\n488- con.execute(\n489- \"\"\"UPDATE listings SET url=:url, title=:title,","numLines":60,"appliedLimit":60}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"a2799743-b94a-4519-8ea8-5e26d7c2598d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLSbNMgy7o885Etcccz","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS8AgKpwEIERgCKkBdHaiCaM2n+6O007G7i4FucQnAMnGPCbm/eAl8MBJxel8AZeZ8ggBTLGccQ9sMEeKp9uk9r+bl+V2VdYJw+kPkMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHggbPUBrABAhIM0LXXPWWdtUuyN9TGGgy3YdU0V8QFZtYo7VMiMMaKPx1WUlsnuqnVohTxKXIEVXRRUkbUkrVc4XqRARc0VI8veUvcFShTxt4tVzi22yr1Bt/zBHqq+KA4panLQ1vz9Jm09YySH9c0zCaQWfMEPJ/nSh6e1vwwFAoifmZV+FpFOIJ3zJvfGo2Ik1vz+oj6rbLPlWdwYonVjl5jEW/m0AHUp/ZGEicn0qRpBMD7bK6Kgc+PUQSZu4jCRfBAXh+FItVlt6hxbi0RO5iNeDmUlK7caztYg+WRk5aqsBjTiZGVzDkSNIbSBMkQrSVwjzFNvnyWODsaKXpVf1hUNhc5dVw7C/ET8QUjzi6FiC2yrixAZBgO62tSf1JJ2Y7pCc0+doApuOhhmvCkgsODY2aSUdedItwNqrjmT//rL9JsEmCtsr9YbbGq5oDe4M+5bMpYOdGGXWsuuofriuwAM6zkPkBxIYCJr5A6ByoTbVGFqxKKTR/JuLUWQwsKQe+V6EOhC3ihXpbPOrdW1oBDu2AgDH8+EOdtO+5rgJGVg3nRl66maIw0JHMMuatclJsIKzgsYqEXq5cXfmNFPa0kG0LA58w41UYQzcXrhehmP3gOrQD/gnKWP7rZtOQcAfuIajN54irbVnY+3BzwWaI0qkFSh8xOlCBvN6K5BMpAdaEhGYkpB5hepOLiP3o/rjET0iuJrNDeURUorabvsK/vovUHZ7vSv7ctuvPmEzMl4hJOLukOr/C9EzexkSVwD6fqv/lwWlOPvZPLZ+Vf6lzCgMWuw6bLjzMYz0G9hJv0s4NorU4MZvU99Swkh8zE3SP/ZZs7uglZ4affcIe60FPwQOS5SGUZqltroBlJihw4veGitlZw/22+ssq8BNfk/X52LugINAJt/9ojpEOJvLxuciaiAN0Ub6uoxslinG/VdLoD3ZFOexR23ct5PxeTbNVjRyyoTzEdqdw+7bEDPEeB6XxYwFiQB+gbjXSQIF/dVOWNwd9hFcgKn7zmwlKac3YnL9YTujYtScLqZ+iKT+Yu8VpRA/jQ7mnAh5jLuwftbl+rOhPBxeq/FPyj9+Yiyctzgv+Y94z8mBGyZAhGAlhjfYHfl43BWvmJdKoA77NdYOXE27BpZlcIyl31dgd4J6rUDErts8qzf1WTmZGAurb2iIqJDtYRylEz0cnuVfefGuGYUc6gIq6k9+51z7seuoUMro8UriT43TxWSnD6tDOraWzj+RgqfI9fioMc9T96XOcDvHjjg3bx7PLSvwWgTM50JT0So+q4BaI6QxgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2183,"cache_read_input_tokens":124538,"cache_creation":{"ephemeral_5m_input_tokens":2183,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"489383e8-f085-40ba-af28-bf55d166246a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLSbNMgy7o885Etcccz","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01G6DAzYRGWWWvaSZJmofCeS","name":"Grep","input":{"pattern":"price_log|row\\[\"price\"\\]","path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","output_mode":"content","-C":4,"offset":486,"head_limit":40},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2183,"cache_read_input_tokens":124538,"cache_creation":{"ephemeral_5m_input_tokens":2183,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"16c7df6d-8b07-4af9-99c9-0e511e0a166a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01G6DAzYRGWWWvaSZJmofCeS","type":"tool_result","content":"No matches found\n\n[Showing results with pagination = offset: 486]"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"743f247c-5fdc-41ad-ae09-dc19cea3454e","timestamp":"2026-08-24T22:08:35.499Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"","numLines":0,"appliedOffset":486}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLSbNMgy7o885Etcccz","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VP1eM9WZjLcdry6uLevFUK","name":"Bash","input":{"command":".venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nprint(con.execute(\\\"SELECT COUNT(*) n FROM price_log WHERE uid='app_urbains:archipel-217'\\\").fetchone()['n'])\nprint(dict(con.execute(\\\"SELECT first_seen, price FROM listings WHERE uid='app_urbains:archipel-217'\\\").fetchone()))\n# combien d'annonces actives sans aucune entrée price_log?\nprint(con.execute('SELECT COUNT(*) n FROM listings l WHERE active=1 AND price IS NOT NULL AND NOT EXISTS (SELECT 1 FROM price_log p WHERE p.uid=l.uid)').fetchone()['n'])\ncon.close()\"","description":"Check price_log coverage gap"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2183,"cache_read_input_tokens":124538,"cache_creation":{"ephemeral_5m_input_tokens":2183,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ffcd4f9b-8729-45d0-9071-6a45d29367e9"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VP1eM9WZjLcdry6uLevFUK","type":"tool_result","content":"0\n{'first_seen': 1786031643.631547, 'price': 1380.0}\n4599","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f90b854a-5410-410f-9234-f14c7e5d575c","timestamp":"2026-08-24T22:08:37.884Z","tool_use_result":{"stdout":"0\n{'first_seen': 1786031643.631547, 'price': 1380.0}\n4599","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"18076d0c-65bc-4c51-9148-28a62f7acf7e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLThn6wpPSwM3NSM8mA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HvDewHYHqK7Q3qFoh5GLqE","name":"Grep","input":{"pattern":"price_log","path":"/Users/simon-pierreboucher/apps/lou-ka/louka/db.py","output_mode":"content","-n":true,"-C":5},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":909,"cache_read_input_tokens":126721,"cache_creation":{"ephemeral_5m_input_tokens":909,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a395ffeb-f29a-44bd-bec6-efec5cf11b02"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HvDewHYHqK7Q3qFoh5GLqE","type":"tool_result","content":"86- payload TEXT, -- JSON opaque propre au connecteur\n87- fetched_at REAL,\n88- PRIMARY KEY (source, external_id)\n89-);\n90-\n91:CREATE TABLE IF NOT EXISTS price_log (\n92- uid TEXT NOT NULL,\n93- ts REAL NOT NULL,\n94- price REAL -- prix observé (NULL = retiré de l'affichage)\n95-);\n96:CREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n97-\n98-CREATE TABLE IF NOT EXISTS poi_cache (\n99- coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n100- lat REAL,\n101- lng REAL,\n--\n179- id INTEGER PRIMARY KEY AUTOINCREMENT,\n180- uid TEXT NOT NULL, -- listings.uid\n181- ts REAL NOT NULL,\n182- event TEXT NOT NULL, -- description | superficie | dispo |\n183- -- inclusions | photos | disparition |\n184: -- reapparition (prix -> price_log)\n185- old TEXT, -- valeur avant (JSON compact)\n186- new TEXT -- valeur après (JSON compact)\n187-);\n188-CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n189-\n--\n359-\n360-def _log_events(con: sqlite3.Connection, uid: str, ts: float,\n361- old: sqlite3.Row, new: dict) -> None:\n362- \"\"\"Journalise les changements observés d'une annonce (listing_events).\n363-\n364: Le prix a déjà son historique dédié (price_log) — ici : description,\n365- superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n366- texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n367- \"\"\"\n368- ev: list[tuple[str, str | None, str | None]] = []\n369- old_desc = (old[\"description\"] or \"\").strip()\n--\n478- :availability_date,:area_sqft,:pets,:furnished,\n479- :description,:digest,:amenities,:details,:images,:lat,:lng,\n480- :content_hash,:now,:now,:now,0,1,\n481- :completeness,:published,:quality_reasons)\"\"\", params)\n482- if lst.price is not None: # prix initial = point de départ de l'historique\n483: con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n484- (lst.uid, now, lst.price))\n485- added += 1\n486- elif row[\"content_hash\"] != h:\n487- # COALESCE : ne jamais écraser des coordonnées géocodées par null\n488- con.execute(\n--\n504- if (row[\"images\"] or \"[]\") != params[\"images\"]:\n505- # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n506- con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n507- \" WHERE uid=?\", (lst.uid,))\n508- if lst.price != row[\"price\"]: # changement de prix -> historique\n509: con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n510- (lst.uid, now, lst.price))\n511- # timeline de la fiche : changements observés (hors prix)\n512- _log_events(con, lst.uid, now, row, params)\n513- if not row[\"active\"]:\n514- con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\""}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"53365de4-f418-47ca-8ada-931848ca3cf0","timestamp":"2026-08-24T22:08:43.178Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"86- payload TEXT, -- JSON opaque propre au connecteur\n87- fetched_at REAL,\n88- PRIMARY KEY (source, external_id)\n89-);\n90-\n91:CREATE TABLE IF NOT EXISTS price_log (\n92- uid TEXT NOT NULL,\n93- ts REAL NOT NULL,\n94- price REAL -- prix observé (NULL = retiré de l'affichage)\n95-);\n96:CREATE INDEX IF NOT EXISTS idx_price_log_uid ON price_log(uid);\n97-\n98-CREATE TABLE IF NOT EXISTS poi_cache (\n99- coord_key TEXT PRIMARY KEY, -- \"lat,lng\" arrondi à 4 décimales (~11 m)\n100- lat REAL,\n101- lng REAL,\n--\n179- id INTEGER PRIMARY KEY AUTOINCREMENT,\n180- uid TEXT NOT NULL, -- listings.uid\n181- ts REAL NOT NULL,\n182- event TEXT NOT NULL, -- description | superficie | dispo |\n183- -- inclusions | photos | disparition |\n184: -- reapparition (prix -> price_log)\n185- old TEXT, -- valeur avant (JSON compact)\n186- new TEXT -- valeur après (JSON compact)\n187-);\n188-CREATE INDEX IF NOT EXISTS idx_levents_uid ON listing_events(uid, ts);\n189-\n--\n359-\n360-def _log_events(con: sqlite3.Connection, uid: str, ts: float,\n361- old: sqlite3.Row, new: dict) -> None:\n362- \"\"\"Journalise les changements observés d'une annonce (listing_events).\n363-\n364: Le prix a déjà son historique dédié (price_log) — ici : description,\n365- superficie, disponibilité, inclusions, photos. Valeurs compactes (pas de\n366- texte intégral) : l'objectif est la timeline de la fiche, pas l'archive.\n367- \"\"\"\n368- ev: list[tuple[str, str | None, str | None]] = []\n369- old_desc = (old[\"description\"] or \"\").strip()\n--\n478- :availability_date,:area_sqft,:pets,:furnished,\n479- :description,:digest,:amenities,:details,:images,:lat,:lng,\n480- :content_hash,:now,:now,:now,0,1,\n481- :completeness,:published,:quality_reasons)\"\"\", params)\n482- if lst.price is not None: # prix initial = point de départ de l'historique\n483: con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n484- (lst.uid, now, lst.price))\n485- added += 1\n486- elif row[\"content_hash\"] != h:\n487- # COALESCE : ne jamais écraser des coordonnées géocodées par null\n488- con.execute(\n--\n504- if (row[\"images\"] or \"[]\") != params[\"images\"]:\n505- # galerie modifiée : invalider l'audit d'images (imgcheck.py)\n506- con.execute(\"UPDATE listings SET images_ok=NULL, img_audit=NULL\"\n507- \" WHERE uid=?\", (lst.uid,))\n508- if lst.price != row[\"price\"]: # changement de prix -> historique\n509: con.execute(\"INSERT INTO price_log (uid, ts, price) VALUES (?,?,?)\",\n510- (lst.uid, now, lst.price))\n511- # timeline de la fiche : changements observés (hors prix)\n512- _log_events(con, lst.uid, now, row, params)\n513- if not row[\"active\"]:\n514- con.execute(\"INSERT INTO listing_events (uid, ts, event, old, new)\"","numLines":64}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"636a5a91-73aa-4c39-b311-00b0c2f1c1cd","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLU69kqXkHKuHTEoSWm","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1519,"cache_read_input_tokens":127630,"cache_creation":{"ephemeral_5m_input_tokens":1519,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3a002f53-4a7b-4893-b3f1-01eb60b08311"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLU69kqXkHKuHTEoSWm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KFwZeXGZrXYmnu1UFcsHtf","name":"Grep","input":{"pattern":"INSERT INTO listings|INSERT OR REPLACE INTO listings","path":"/Users/simon-pierreboucher/apps/lou-ka/louka","output_mode":"content","-n":true},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1519,"cache_read_input_tokens":127630,"cache_creation":{"ephemeral_5m_input_tokens":1519,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"69555adb-e78c-494f-b9c4-c13533027286"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KFwZeXGZrXYmnu1UFcsHtf","type":"tool_result","content":"louka/db.py:467: \"\"\"INSERT INTO listings (uid, source, external_id, url, title,"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b337e301-fe52-4b88-816c-1a5451bf8db9","timestamp":"2026-08-24T22:09:04.543Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"louka/db.py:467: \"\"\"INSERT INTO listings (uid, source, external_id, url, title,","numLines":1}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLU69kqXkHKuHTEoSWm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01GaTy2rzHPrxwXCqW9iCZst","name":"Bash","input":{"command":".venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\n# les 4599 sans price_log : répartition par source et par date d'apparition\nrows = con.execute('''SELECT source, COUNT(*) n, MIN(first_seen) mn, MAX(first_seen) mx FROM listings l\n WHERE active=1 AND price IS NOT NULL AND NOT EXISTS (SELECT 1 FROM price_log p WHERE p.uid=l.uid)\n GROUP BY source ORDER BY n DESC LIMIT 8''').fetchall()\nimport datetime\nfor r in rows: print(r['source'], r['n'], datetime.date.fromtimestamp(r['mn']), datetime.date.fromtimestamp(r['mx']))\ncon.close()\"","description":"Profile listings missing price_log"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1519,"cache_read_input_tokens":127630,"cache_creation":{"ephemeral_5m_input_tokens":1519,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d39209e8-f9a5-4618-85bb-f471c6fd3e1d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01GaTy2rzHPrxwXCqW9iCZst","type":"tool_result","content":"equinoxe_batimo 617 2026-08-06 2026-08-07\nlogis_experts 480 2026-08-08 2026-08-08\ndevimco 423 2026-08-06 2026-08-07\ngroupe_dallaire 254 2026-08-06 2026-08-06\ncogir 247 2026-08-06 2026-08-08\ncontraste 177 2026-08-06 2026-08-06\nedifia 164 2026-08-08 2026-08-08\nsgiq 147 2026-08-06 2026-08-06","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c092f83b-0be8-4a54-aeca-e027503787ce","timestamp":"2026-08-24T22:09:07.316Z","tool_use_result":{"stdout":"equinoxe_batimo 617 2026-08-06 2026-08-07\nlogis_experts 480 2026-08-08 2026-08-08\ndevimco 423 2026-08-06 2026-08-07\ngroupe_dallaire 254 2026-08-06 2026-08-06\ncogir 247 2026-08-06 2026-08-08\ncontraste 177 2026-08-06 2026-08-06\nedifia 164 2026-08-08 2026-08-08\nsgiq 147 2026-08-06 2026-08-06","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"e7c79d0a-71d2-47e4-b4ea-48e73a452d39","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLVscFaxkzXyp42Ha4u","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISywoKpwEIERgCKkC7PQXdqMt167HU69kdkQixRYtDG3eJ42g9N4hpC4uLlRPgh9chIYcuUAhdyBI82q9Q/yQxnnQcUATyL0LSNA5CMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGOgrPUBrABAhIMzAKsjI6Y3Gc6wByAGgwOfkIiWyBPYHCDUc8iMAtcVztjB5nlr+ZJFg1kWuhEgihK4qXa6WCrqtgxakxSDrYG6UEtadNorNbKFCXCVCrQCKPTbL1r2OvsNXbUZKMObqURzEDMBmy5KpuxXBaR//a2fPsLYSAxIs7k+riwLDtDcDtGNTIpqQUQtYEZTBdHZv1haahVE9jB76FsoWAHsoU6ajDSyi5hyXbZyqFuJw8VNwhXUcZ0bnqXaKcqOOUeRKN8ApfL3wVPqFdhhjsuU3liq1QGpxfR9DS5b/rnmc851J69F9VJMSCALkymHUQyn/j0ZlTwh+eT/frzjkJsFENjI5yAGpURp4Ogf4Pl4GoF42N1V6bUHRA90+UMIKT7t5N2ZNcefpZOKAOiNGHS5CwH7kbqFPFk8UgV8/1E1dwayXG6brk2dj3zSAgzwuxUdXZPokuCmjuIDFdYvLwLA5TvaEkY6mm3LJZALo/IJ40QRpLBjBwJ81H2qjJPZz1+Nmy/s/wzqzK7l+0Xtb8kRsjXx9dY92ePclGly7tXOCsJ9+sf6O4VSNnyq2E4PSQIcmy+8U4iyrSHB4DxWEBeQ0XFEuHujtiqOTP7JPt8nNx9ZLyL/BsMQLLUn37ePYlJzLP8ak1+4HXVnd/T16a4QEksfpX6MBokQufWCFOAtIcWFa3LmIkEamXayTmiiXVLekbKEFgXeMNd5bdT2wrLxvjObYD3s1NEuXnx5/d1Pq3WJzpzbJPdqHxj7JyynSN7Wn86orlKriRUI3SqZAYRGtZKmXOX7Bj2K0PpzxEyu+F1UTOn+ux7ws4eMoIVspr8KuM+oOFlGJRHvrlDIXzo1E9SepGWaD1sslY1S9sItrtyWVc6ABa503Z9oO0y0xtGQje5cPz8WT0Ud0RTsr3jXPLZB2HkRNq4qzXEWzuDxbyS4C3BvZSKvqe1UhqEGIY19VdTIEMebipoTIRsMWI5EKh0VLslEw5iw/NCTXStUXVCQNpZ6i5icHnVN2e2X/fslUyE0J7WJ8oqgE0jhdHZN9wjMAPM2Yz/MXBeijSIEDcYOMg1GDWju6m/qJ2+J3EaJyDWc2lp0VLibgiAWf+Rf8GuJXfB2rrK0UENeDSFNP9Hr4xYupD2DsU5n4RG1A6WDuauGask2SAzm4RmXWam3VdiX8FVhSD2xUM5E00tqPUofMDKPvwgqLwntH/SW+JUTDJ1HwWwHiZqdfpavX2OG1gxszWiwSYMG70KkQcNb7WppDMJzPfdGQNWqjTNnofIHJS5LjyR3knVoowH8E15J4vW5Is0Lsz3QBkXge/6kNr3ZsxbA70G/q0HXLjKF0sf36zMHuWAAY6G2Rv9Wp+zoxO2u5P9i4Cy4NIKyOdB3R/VqbEoAVGHtpfnrAzgbydFHgfC4g6mEWgCmFyed1w8RPwBlIuEByry/W6xQlENsQZ5lktXaEGGPZhk1uVKRDvLav+4Glk0dYnjTsXSMZzExoB1pOKqaYAzuw8L4biIeFDRDohCSsllkn6IWabCIiWeHE3WtiZiTEX1ceMyJ7x3HlHF0tM5nPShLCeyc6mcykuJ0hgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1674,"cache_read_input_tokens":129149,"cache_creation":{"ephemeral_5m_input_tokens":1674,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"688141bc-2c3b-499e-a9fe-92588bf9feb7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLVscFaxkzXyp42Ha4u","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS2wMKqAEIERgCKkAOoArOGMVz0UxKYkFNIl0F3qegUzBOq/FYVFxXWBH7XTggeXDDsfoJ1POq3N2bmFHfnrhypcRWtcj8l88eHaxlMg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagBjoKz1AawAQISDBLYFIJhBritRs7B4BoMTUl2Zv98D0EZK3KuIjDxH7fdZVhDAvgFWbzTHTw6rzRuTDxQZab/4cX2wEB5C7x60mphchgSsIwl8CWQCgcq3wEMYmdJ8sbusQE8NhZsD2wsXU/SwgBmbrIPvaMLqomhowV6Dd7s7dTUw5XudlC/E5/asPbqtpVGgqt95eIeshwW37sSnEefV38bpZ1yXAwty6/AgUh8XuUyav2G/pz0rrvKUMi+1QjKe5fbiOENqpUcWx/9cfEKElf7s7giQZywHujehwnmjWnPVrjCDJdT9VLqgykNZ3k+7rlMCnDdn3P5CTHxZyDRcI0ljMS5yCRMiY0Xs+Ip5yrTj5MTSNb56RqB9XCZjkyJGpvAIdFSmRgJQ8xbxkwDR3+S9o3kS+qcGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1674,"cache_read_input_tokens":129149,"cache_creation":{"ephemeral_5m_input_tokens":1674,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8dcf5306-8b91-4628-9f63-6f3c408175a1"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLVscFaxkzXyp42Ha4u","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0111F7v7i69qMgE3KYrq1spn","name":"Bash","input":{"command":".venv/bin/python -m py_compile louka/web.py louka/ingest.py run.py && .venv/bin/python -c \"from louka import web; print('web OK, routes:', sum(1 for r in web.app.routes))\"","description":"Compile check + import web app"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1674,"cache_read_input_tokens":129149,"cache_creation":{"ephemeral_5m_input_tokens":1674,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fcbb6fea-89c6-40b9-bfd6-3441b709149b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0111F7v7i69qMgE3KYrq1spn","type":"tool_result","content":"web OK, routes: 44","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1757666d-475c-4a55-adb7-41bfa9e58f80","timestamp":"2026-08-24T22:09:19.785Z","tool_use_result":{"stdout":"web OK, routes: 44","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"509f589a-b69d-45ea-9e1c-100f9b4c0277","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLWnceLspRJUgCPFfwM","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01AwuABLJkrrYxQsT5EtDLqW","name":"TodoWrite","input":{"todos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"completed","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"completed","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"in_progress","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":602,"cache_read_input_tokens":130823,"cache_creation":{"ephemeral_5m_input_tokens":602,"ephemeral_1h_input_tokens":0},"output_tokens":25,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0a122c1c-8cb4-4a72-97d8-8fc3377ad656"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01AwuABLJkrrYxQsT5EtDLqW","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"61a24e6c-fb94-413b-a0b5-d2ace1eed8f6","timestamp":"2026-08-24T22:09:29.510Z","tool_use_result":{"oldTodos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"completed","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"in_progress","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"pending","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Build et redéploiement"}],"newTodos":[{"content":"Migrations db.py (listing_events, buildings, recycled_cache, managers, manager_reviews) + event logging","status":"completed","activeForm":"Écriture des migrations db.py"},{"content":"Module history.py (timeline observée)","status":"completed","activeForm":"Écriture de history.py"},{"content":"Module building.py (passeport immeuble, rotation, pression loyers)","status":"completed","activeForm":"Écriture de building.py"},{"content":"Module recycled.py (annonces recyclées multi-signaux)","status":"completed","activeForm":"Écriture de recycled.py"},{"content":"Module coutreel.py (coût réel mensuel + pi²/percentiles)","status":"completed","activeForm":"Écriture de coutreel.py"},{"content":"Module hiver.py (score vie quotidienne en hiver)","status":"completed","activeForm":"Écriture de hiver.py"},{"content":"Modules serpapi_client.py + managers.py (gestionnaire + avis Google Maps)","status":"completed","activeForm":"Écriture de serpapi_client.py et managers.py"},{"content":"Endpoints web.py + CLI run.py + hooks ingest.watch()","status":"completed","activeForm":"Câblage API/CLI/watch"},{"content":"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS","status":"in_progress","activeForm":"Écriture des composants frontend"},{"content":"building.rollup() initial + tests curl des endpoints","status":"pending","activeForm":"Rollup initial et tests"},{"content":"Build frontend, pm2 restart, healthcheck","status":"pending","activeForm":"Build et redéploiement"},{"content":"Commit + push origin main","status":"pending","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"97488934-55bd-4c13-bbb2-d17f8b6f5354","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLXW6kPf1RyBsEtiDdS","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01LkVJ3ZxLECdYaU4QU1Wc6W","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueTAL.tsx"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":861,"cache_read_input_tokens":131425,"cache_creation":{"ephemeral_5m_input_tokens":861,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9945e04a-8313-4e34-abf7-372cba19bc38"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01LkVJ3ZxLECdYaU4QU1Wc6W","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// components/HistoriqueTAL.tsx : bloc « Historique TAL » (fiche)\n5\t// Décisions du Tribunal administratif du logement (diffusées par SOQUIJ)\n6\t// rendues à l'adresse de l'annonce : le locataire potentiel voit si\n7\t// l'immeuble a un historique de litiges (non-paiement, résiliation,\n8\t// expulsion, reprise/éviction, insalubrité…) et qui était à l'initiative\n9\t// du recours (propriétaire ou locataire). Par respect de la vie privée,\n10\t// aucun nom de partie n'est affiché — seulement la référence neutre\n11\t// (ex. « 2026 QCTAL 21509 ») avec lien vers le texte intégral sur SOQUIJ.\n12\t// -----------------------------------------------------------------------------\n13\timport { useEffect, useState } from \"react\";\n14\timport { fetchTal, TalHistory } from \"../api\";\n15\t\n16\tconst fmtDate = (d: string | null): string => {\n17\t if (!d) return \"—\";\n18\t const dt = new Date(d + \"T00:00:00\");\n19\t return Number.isNaN(dt.getTime())\n20\t ? d\n21\t : dt.toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" });\n22\t};\n23\t\n24\texport default function HistoriqueTAL({ address, city }:\n25\t { address: string | null; city: string | null }) {\n26\t const [d, setD] = useState(null);\n27\t useEffect(() => {\n28\t setD(null);\n29\t if (!address) return;\n30\t fetchTal(address, city).then(setD).catch(() => setD(null));\n31\t }, [address, city]);\n32\t if (!address || !d || d.status === \"na\" || d.status === \"error\") return null;\n33\t\n34\t const decisions = d.decisions ?? [];\n35\t const alerte = (d.eviction ?? 0) > 0 || (d.contre_locataire ?? 0) > 0;\n36\t return (\n37\t
\n38\t

Historique au TAL

\n39\t {d.status === \"pending\" && (\n40\t

\n41\t Vérification en cours{\" \"}\n42\t Cette adresse est en file de vérification auprès des décisions\n43\t publiées du Tribunal administratif du logement.\n44\t

\n45\t )}\n46\t {d.status === \"ok\" && decisions.length === 0 && (\n47\t

\n48\t Aucune décision trouvée{\" \"}\n49\t Aucune décision publiée du Tribunal administratif du logement n'a\n50\t été repérée à cette adresse.\n51\t

\n52\t )}\n53\t {d.status === \"ok\" && decisions.length > 0 && (\n54\t <>\n55\t

\n56\t \n57\t {d.n} décision{(d.n ?? 0) > 1 ? \"s\" : \"\"} au TAL\n58\t {\" \"}\n59\t {d.contre_locataire\n60\t ? `dont ${d.contre_locataire} à l'initiative du propriétaire`\n61\t : \"aucune à l'initiative du propriétaire\"}\n62\t {d.last_date ? ` — la plus récente : ${fmtDate(d.last_date)}` : \"\"}.\n63\t

\n64\t
    \n65\t {decisions.slice(0, 8).map((dec) => (\n66\t
  • \n67\t
    \n68\t {fmtDate(dec.date)}\n69\t {dec.demandeur && (\n70\t \n71\t demande du {dec.demandeur}\n72\t \n73\t )}\n74\t {dec.verdict && (\n75\t demande {dec.verdict}\n76\t )}\n77\t
    \n78\t {dec.tags.length > 0 && (\n79\t
    \n80\t {dec.tags.map((t) => (\n81\t {t}\n82\t ))}\n83\t
    \n84\t )}\n85\t \n87\t {dec.citation || \"Texte intégral\"} — SOQUIJ\n88\t \n89\t
  • \n90\t ))}\n91\t
\n92\t \n93\t )}\n94\t

\n95\t Décisions publiques du{\" \"}\n96\t Tribunal administratif du logement,\n98\t diffusées par SOQUIJ (citoyens.soquij.qc.ca) et repérées par\n99\t correspondance d'adresse dans le texte des décisions — vérification\n100\t automatisée fournie à titre indicatif, sans valeur juridique. Une\n101\t décision peut concerner un autre logement du même immeuble.\n102\t

\n103\t
\n104\t );\n105\t}\n106\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"17ededf9-dccd-4b8e-8d19-ec562edb64c4","timestamp":"2026-08-24T22:09:33.939Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueTAL.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/HistoriqueTAL.tsx : bloc « Historique TAL » (fiche)\n// Décisions du Tribunal administratif du logement (diffusées par SOQUIJ)\n// rendues à l'adresse de l'annonce : le locataire potentiel voit si\n// l'immeuble a un historique de litiges (non-paiement, résiliation,\n// expulsion, reprise/éviction, insalubrité…) et qui était à l'initiative\n// du recours (propriétaire ou locataire). Par respect de la vie privée,\n// aucun nom de partie n'est affiché — seulement la référence neutre\n// (ex. « 2026 QCTAL 21509 ») avec lien vers le texte intégral sur SOQUIJ.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { fetchTal, TalHistory } from \"../api\";\n\nconst fmtDate = (d: string | null): string => {\n if (!d) return \"—\";\n const dt = new Date(d + \"T00:00:00\");\n return Number.isNaN(dt.getTime())\n ? d\n : dt.toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" });\n};\n\nexport default function HistoriqueTAL({ address, city }:\n { address: string | null; city: string | null }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n if (!address) return;\n fetchTal(address, city).then(setD).catch(() => setD(null));\n }, [address, city]);\n if (!address || !d || d.status === \"na\" || d.status === \"error\") return null;\n\n const decisions = d.decisions ?? [];\n const alerte = (d.eviction ?? 0) > 0 || (d.contre_locataire ?? 0) > 0;\n return (\n
\n

Historique au TAL

\n {d.status === \"pending\" && (\n

\n Vérification en cours{\" \"}\n Cette adresse est en file de vérification auprès des décisions\n publiées du Tribunal administratif du logement.\n

\n )}\n {d.status === \"ok\" && decisions.length === 0 && (\n

\n Aucune décision trouvée{\" \"}\n Aucune décision publiée du Tribunal administratif du logement n'a\n été repérée à cette adresse.\n

\n )}\n {d.status === \"ok\" && decisions.length > 0 && (\n <>\n

\n \n {d.n} décision{(d.n ?? 0) > 1 ? \"s\" : \"\"} au TAL\n {\" \"}\n {d.contre_locataire\n ? `dont ${d.contre_locataire} à l'initiative du propriétaire`\n : \"aucune à l'initiative du propriétaire\"}\n {d.last_date ? ` — la plus récente : ${fmtDate(d.last_date)}` : \"\"}.\n

\n
    \n {decisions.slice(0, 8).map((dec) => (\n
  • \n
    \n {fmtDate(dec.date)}\n {dec.demandeur && (\n \n demande du {dec.demandeur}\n \n )}\n {dec.verdict && (\n demande {dec.verdict}\n )}\n
    \n {dec.tags.length > 0 && (\n
    \n {dec.tags.map((t) => (\n {t}\n ))}\n
    \n )}\n \n {dec.citation || \"Texte intégral\"} — SOQUIJ\n \n
  • \n ))}\n
\n \n )}\n

\n Décisions publiques du{\" \"}\n Tribunal administratif du logement,\n diffusées par SOQUIJ (citoyens.soquij.qc.ca) et repérées par\n correspondance d'adresse dans le texte des décisions — vérification\n automatisée fournie à titre indicatif, sans valeur juridique. Une\n décision peut concerner un autre logement du même immeuble.\n

\n
\n );\n}\n","numLines":106,"startLine":1,"totalLines":106}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"71b91ed7-e793-451e-9975-bd447c78a670","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLXqAxag4Hyd5Na9Qm4","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_018qMfiXyDHv766vfT9G8P9J","name":"Bash","input":{"command":"grep -n \"fetchTal\\|export async function\\|export interface TalHistory\\|API_BASE\\|export const\" frontend/src/api.ts | head -40; echo ---; wc -l frontend/src/api.ts frontend/src/pages/Listing.tsx frontend/src/styles.css 2>/dev/null; ls frontend/src/components/","description":"Map frontend api.ts and components"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2107,"cache_read_input_tokens":132286,"cache_creation":{"ephemeral_5m_input_tokens":2107,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e8fdfdf3-d699-445f-94ad-a7a8f35fa108"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_018qMfiXyDHv766vfT9G8P9J","type":"tool_result","content":"151:export const fetchKaScoresStats = () => get(\"/api/kascores/stats\");\n164:export const KA_DEFAULT_WEIGHTS: Record = {\n206:export const fetchFairValue = (uid: string) =>\n222:export const fetchInondation = (lat: number, lng: number) =>\n240:export const fetchRdl = (lat: number, lng: number, radius = 600) =>\n252:export interface TalHistory {\n263:export const fetchTal = (address: string, city?: string | null) =>\n268:export const fmtDist = (m: number): string =>\n374:export async function fetchSearch(\n435:export const fetchDetailedStats = () => get(\"/api/stats/detailed\");\n437:export const fetchListing = (uid: string) =>\n439:export const fetchFacets = (city?: string) =>\n448:export const fetchSources = () => get<{ sources: Source[] }>(\"/api/sources\");\n449:export const fetchStats = () => get(\"/api/stats\");\n485:export const setRole = (role: \"locataire\" | \"gestionnaire\") =>\n488:export const updateMyProfile = (fields: {\n497:export async function uploadAvatar(file: File): Promise {\n504:export const deleteAvatar = () => fetch(\"/api/me/avatar\", { method: \"DELETE\" });\n507:export const fetchFavorites = () =>\n509:export const addFavorite = (uid: string) =>\n511:export const removeFavorite = (uid: string) =>\n527:export const fetchMyOrg = () =>\n530:export const claimOrg = (source: string) =>\n532:export const updateOrg = (fields: {\n540:export const fetchOrg = (sourceId: string) =>\n560:export const fetchPublicProfile = (kaId: string) =>\n562:export const setPublicProfile = (enabled: boolean) =>\n565:export async function fetchMe(): Promise {\n573:export const fetchAuthConfig = () => get<{ google: boolean; ka: boolean }>(\"/api/auth/config\");\n574:export const logout = () => fetch(\"/api/auth/logout\", { method: \"POST\" });\n576:export const fmtPrice = (p: number | null, label?: string) =>\n597:export const thumb = (u: string, w: 160 | 480 | 800 | 1280 = 480) =>\n634:export const fetchAir = (lat: number, lng: number) =>\n649:export const fetchGaz = (lat: number, lng: number) =>\n662:export const fetchCommerces = (lat: number, lng: number) =>\n671:export const fetchHydro = (\n---\n 682 frontend/src/api.ts\n 556 frontend/src/pages/Listing.tsx\n 2115 frontend/src/styles.css\n 3353 total\nCommercesProches.tsx\nCookieConsent.tsx\nCtFicheMap.tsx\nCtListingCard.tsx\nCtMapView.tsx\nEssenceProche.tsx\nFairValueBadge.tsx\nHistoriqueTAL.tsx\nHydroEstimation.tsx\nIcons.tsx\nKaScoreBadge.tsx\nKaScoresBlock.tsx\nListingCard.tsx\nListingMap3D.tsx\nLogo.tsx\nMapView.tsx\nMetroLignes.tsx\nPager.tsx\nPriceAnalysis.tsx\nQualiteAir.tsx\nQuartierBlock.tsx\nRegistreLoyers.tsx\nRisqueInondation.tsx\nSmartImg.tsx","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5e593161-b14c-4987-a639-c31b872608b6","timestamp":"2026-08-24T22:09:40.816Z","tool_use_result":{"stdout":"151:export const fetchKaScoresStats = () => get(\"/api/kascores/stats\");\n164:export const KA_DEFAULT_WEIGHTS: Record = {\n206:export const fetchFairValue = (uid: string) =>\n222:export const fetchInondation = (lat: number, lng: number) =>\n240:export const fetchRdl = (lat: number, lng: number, radius = 600) =>\n252:export interface TalHistory {\n263:export const fetchTal = (address: string, city?: string | null) =>\n268:export const fmtDist = (m: number): string =>\n374:export async function fetchSearch(\n435:export const fetchDetailedStats = () => get(\"/api/stats/detailed\");\n437:export const fetchListing = (uid: string) =>\n439:export const fetchFacets = (city?: string) =>\n448:export const fetchSources = () => get<{ sources: Source[] }>(\"/api/sources\");\n449:export const fetchStats = () => get(\"/api/stats\");\n485:export const setRole = (role: \"locataire\" | \"gestionnaire\") =>\n488:export const updateMyProfile = (fields: {\n497:export async function uploadAvatar(file: File): Promise {\n504:export const deleteAvatar = () => fetch(\"/api/me/avatar\", { method: \"DELETE\" });\n507:export const fetchFavorites = () =>\n509:export const addFavorite = (uid: string) =>\n511:export const removeFavorite = (uid: string) =>\n527:export const fetchMyOrg = () =>\n530:export const claimOrg = (source: string) =>\n532:export const updateOrg = (fields: {\n540:export const fetchOrg = (sourceId: string) =>\n560:export const fetchPublicProfile = (kaId: string) =>\n562:export const setPublicProfile = (enabled: boolean) =>\n565:export async function fetchMe(): Promise {\n573:export const fetchAuthConfig = () => get<{ google: boolean; ka: boolean }>(\"/api/auth/config\");\n574:export const logout = () => fetch(\"/api/auth/logout\", { method: \"POST\" });\n576:export const fmtPrice = (p: number | null, label?: string) =>\n597:export const thumb = (u: string, w: 160 | 480 | 800 | 1280 = 480) =>\n634:export const fetchAir = (lat: number, lng: number) =>\n649:export const fetchGaz = (lat: number, lng: number) =>\n662:export const fetchCommerces = (lat: number, lng: number) =>\n671:export const fetchHydro = (\n---\n 682 frontend/src/api.ts\n 556 frontend/src/pages/Listing.tsx\n 2115 frontend/src/styles.css\n 3353 total\nCommercesProches.tsx\nCookieConsent.tsx\nCtFicheMap.tsx\nCtListingCard.tsx\nCtMapView.tsx\nEssenceProche.tsx\nFairValueBadge.tsx\nHistoriqueTAL.tsx\nHydroEstimation.tsx\nIcons.tsx\nKaScoreBadge.tsx\nKaScoresBlock.tsx\nListingCard.tsx\nListingMap3D.tsx\nLogo.tsx\nMapView.tsx\nMetroLignes.tsx\nPager.tsx\nPriceAnalysis.tsx\nQualiteAir.tsx\nQuartierBlock.tsx\nRegistreLoyers.tsx\nRisqueInondation.tsx\nSmartImg.tsx","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f059bb4e-9ba3-4484-a59d-a762bf1caefa","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLYLSMMLcRYpqzqa6np","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01V3HakHYz4SbZzfwmUoimX2","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","offset":240,"limit":30},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1409,"cache_read_input_tokens":134393,"cache_creation":{"ephemeral_5m_input_tokens":1409,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a81d1487-73bb-4d6a-8379-2ae0c7eb5da1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01V3HakHYz4SbZzfwmUoimX2","type":"tool_result","content":"240\texport const fetchRdl = (lat: number, lng: number, radius = 600) =>\n241\t get(`/api/rdl?lat=${lat}&lng=${lng}&radius=${radius}`);\n242\t\n243\texport interface TalDecision {\n244\t date: string | null;\n245\t citation: string | null;\n246\t demandeur: \"locateur\" | \"locataire\" | null;\n247\t tags: string[];\n248\t verdict: \"accueillie\" | \"rejetée\" | null;\n249\t url: string;\n250\t}\n251\t\n252\texport interface TalHistory {\n253\t status: \"ok\" | \"pending\" | \"error\" | \"na\";\n254\t checked_at?: string;\n255\t n?: number;\n256\t contre_locataire?: number;\n257\t eviction?: number;\n258\t last_date?: string | null;\n259\t decisions?: TalDecision[];\n260\t}\n261\t\n262\t/** Décisions du Tribunal administratif du logement (SOQUIJ) à l'adresse. */\n263\texport const fetchTal = (address: string, city?: string | null) =>\n264\t get(`/api/tal?address=${encodeURIComponent(address)}` +\n265\t (city ? `&city=${encodeURIComponent(city)}` : \"\"));\n266\t\n267\t/** 250 -> « 250 m », 1240 -> « 1,2 km » */\n268\texport const fmtDist = (m: number): string =>\n269\t m < 1000 ? `${Math.round(m / 10) * 10} m` : `${(m / 1000).toFixed(1).replace(\".\", \",\")} km`;"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c80c183e-61e7-49e6-9898-223fe447931f","timestamp":"2026-08-24T22:09:50.528Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","content":"export const fetchRdl = (lat: number, lng: number, radius = 600) =>\n get(`/api/rdl?lat=${lat}&lng=${lng}&radius=${radius}`);\n\nexport interface TalDecision {\n date: string | null;\n citation: string | null;\n demandeur: \"locateur\" | \"locataire\" | null;\n tags: string[];\n verdict: \"accueillie\" | \"rejetée\" | null;\n url: string;\n}\n\nexport interface TalHistory {\n status: \"ok\" | \"pending\" | \"error\" | \"na\";\n checked_at?: string;\n n?: number;\n contre_locataire?: number;\n eviction?: number;\n last_date?: string | null;\n decisions?: TalDecision[];\n}\n\n/** Décisions du Tribunal administratif du logement (SOQUIJ) à l'adresse. */\nexport const fetchTal = (address: string, city?: string | null) =>\n get(`/api/tal?address=${encodeURIComponent(address)}` +\n (city ? `&city=${encodeURIComponent(city)}` : \"\"));\n\n/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>\n m < 1000 ? `${Math.round(m / 10) * 10} m` : `${(m / 1000).toFixed(1).replace(\".\", \",\")} km`;","numLines":30,"startLine":240,"totalLines":683}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLYLSMMLcRYpqzqa6np","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Fh6CxbuvXRA7jaG9QxVxtY","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1409,"cache_read_input_tokens":134393,"cache_creation":{"ephemeral_5m_input_tokens":1409,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"69a93992-2c8d-4c13-9865-9f79b506829e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Fh6CxbuvXRA7jaG9QxVxtY","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n5\t// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n6\t// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n7\t// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n8\t// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n9\t// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n10\t// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n11\t// blocs sans `order` passaient devant la galerie sur mobile).\n12\t// -----------------------------------------------------------------------------\n13\timport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\n14\timport { Link, useParams } from \"react-router-dom\";\n15\timport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\n16\timport QuartierBlock from \"../components/QuartierBlock\";\n17\timport SmartImg from \"../components/SmartImg\";\n18\timport FairValueBadge from \"../components/FairValueBadge\";\n19\timport PriceAnalysis from \"../components/PriceAnalysis\";\n20\timport HistoriqueTAL from \"../components/HistoriqueTAL\";\n21\timport RegistreLoyers from \"../components/RegistreLoyers\";\n22\timport RisqueInondation from \"../components/RisqueInondation\";\n23\timport QualiteAir from \"../components/QualiteAir\";\n24\timport EssenceProche from \"../components/EssenceProche\";\n25\timport HydroEstimation from \"../components/HydroEstimation\";\n26\timport CommercesProches from \"../components/CommercesProches\";\n27\timport { IcoAlert, IcoDoc } from \"../components/Icons\";\n28\timport KaScoresBlock from \"../components/KaScoresBlock\";\n29\timport { markSeen } from \"../search/seen\";\n30\t\n31\t// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\n32\tconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n33\t\n34\t// Icônes et libellés des commodités de proximité (louka/poi.py)\n35\tconst POI_META: Record = {\n36\t epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n37\t depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n38\t pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n39\t ecole: { icon: \"🏫\", label: \"École\" },\n40\t garderie: { icon: \"🧸\", label: \"Garderie\" },\n41\t parc: { icon: \"🌳\", label: \"Parc\" },\n42\t bus: { icon: \"🚌\", label: \"Bus\" },\n43\t metro: { icon: \"🚇\", label: \"Métro\" },\n44\t gym: { icon: \"🏋️\", label: \"Gym\" },\n45\t cafe: { icon: \"☕\", label: \"Café\" },\n46\t clinique: { icon: \"🩺\", label: \"Clinique\" },\n47\t hopital: { icon: \"🏥\", label: \"Hôpital\" },\n48\t bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n49\t};\n50\t\n51\t// Regroupement des POI en catégories repliables\n52\tconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n53\t { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n54\t { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n55\t { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n56\t { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n57\t { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n58\t];\n59\t\n60\t// Badge « prix vs marché » — seuils configurables\n61\tconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n62\t\n63\tconst PETS_LABEL: Record = {\n64\t oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n65\t};\n66\t\n67\tconst NBSP = \" \";\n68\t\n69\t/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\n70\tconst fmtMarche = (m: number): string =>\n71\t `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n72\t\n73\tfunction badgeMarche(price: number | null | undefined,\n74\t loyerSecteur: number | null | undefined) {\n75\t if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n76\t const delta = (price - loyerSecteur) / loyerSecteur;\n77\t const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n78\t if (delta <= SEUILS_MARCHE.bonDeal)\n79\t return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n80\t if (delta <= SEUILS_MARCHE.dansLeMarche)\n81\t return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n82\t return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n83\t}\n84\t\n85\t/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\n86\tfunction badgesConfirmes(l: Listing): string[] {\n87\t const d = l.details ?? {};\n88\t const out: string[] = [];\n89\t const inc = d.inclusions ?? {};\n90\t if (inc.heating) out.push(\"Chauffage inclus\");\n91\t if (inc.electricity) out.push(\"Électricité incluse\");\n92\t if (inc.hot_water) out.push(\"Eau chaude incluse\");\n93\t if (inc.internet) out.push(\"Internet inclus\");\n94\t const app = d.appliances ?? {};\n95\t if (app.dishwasher) out.push(\"Lave-vaisselle\");\n96\t if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n97\t if (app.fridge && app.stove) out.push(\"Électroménagers\");\n98\t if (d.ac) out.push(\"Climatisation\");\n99\t if (d.elevator) out.push(\"Ascenseur\");\n100\t if (d.balcony) out.push(\"Balcon\");\n101\t if (d.pool) out.push(\"Piscine\");\n102\t if (d.gym) out.push(\"Gym\");\n103\t if (d.laundry) out.push(\"Buanderie\");\n104\t if (d.storage) out.push(\"Rangement\");\n105\t if (d.parking?.available)\n106\t out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n107\t if (l.furnished) out.push(\"Meublé\");\n108\t if (d.smoking === false) out.push(\"Non-fumeur\");\n109\t return out;\n110\t}\n111\t\n112\t// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\n113\tfunction Lightbox({ images, start, titre, onClose }:\n114\t { images: string[]; start: number; titre: string; onClose: () => void }) {\n115\t const [idx, setIdx] = useState(start);\n116\t const [scale, setScale] = useState(1);\n117\t const [tx, setTx] = useState(0);\n118\t const [ty, setTy] = useState(0);\n119\t const track = useRef(null);\n120\t const pointers = useRef(new Map());\n121\t const pinch = useRef<{ d: number; scale: number } | null>(null);\n122\t const lastTap = useRef(0);\n123\t\n124\t useEffect(() => { // position initiale + verrou du défilement de la page\n125\t track.current?.scrollTo({ left: start * track.current.clientWidth });\n126\t document.body.style.overflow = \"hidden\";\n127\t const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n128\t window.addEventListener(\"keydown\", onKey);\n129\t return () => {\n130\t document.body.style.overflow = \"\";\n131\t window.removeEventListener(\"keydown\", onKey);\n132\t };\n133\t // eslint-disable-next-line react-hooks/exhaustive-deps\n134\t }, []);\n135\t\n136\t const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n137\t const onScroll = () => {\n138\t const el = track.current;\n139\t if (el && scale === 1) {\n140\t const i = Math.round(el.scrollLeft / el.clientWidth);\n141\t if (i !== idx) { setIdx(i); resetZoom(); }\n142\t }\n143\t };\n144\t const dist = () => {\n145\t const [a, b] = [...pointers.current.values()];\n146\t return Math.hypot(a.x - b.x, a.y - b.y);\n147\t };\n148\t const onPointerDown = (e: React.PointerEvent) => {\n149\t pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n150\t if (pointers.current.size === 2)\n151\t pinch.current = { d: dist(), scale };\n152\t if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n153\t const now = Date.now();\n154\t if (now - lastTap.current < 300) {\n155\t if (scale > 1) resetZoom(); else setScale(2.5);\n156\t }\n157\t lastTap.current = now;\n158\t }\n159\t };\n160\t const onPointerMove = (e: React.PointerEvent) => {\n161\t const prev = pointers.current.get(e.pointerId);\n162\t if (!prev) return;\n163\t pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n164\t if (pointers.current.size === 2 && pinch.current) {\n165\t const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n166\t setScale(s);\n167\t if (s === 1) { setTx(0); setTy(0); }\n168\t } else if (pointers.current.size === 1 && scale > 1) {\n169\t setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n170\t setTy((v) => v + (e.clientY - prev.y));\n171\t }\n172\t };\n173\t const onPointerUp = (e: React.PointerEvent) => {\n174\t pointers.current.delete(e.pointerId);\n175\t if (pointers.current.size < 2) pinch.current = null;\n176\t };\n177\t\n178\t return (\n179\t
\n181\t \n182\t \n183\t {idx + 1}/{images.length}\n184\t \n185\t 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n188\t onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n189\t onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n190\t >\n191\t {images.map((u, i) => (\n192\t
\n193\t 1\n197\t ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n198\t : undefined}\n199\t />\n200\t
\n201\t ))}\n202\t
\n203\t {scale === 1 && idx > 0 && (\n204\t \n207\t )}\n208\t {scale === 1 && idx < images.length - 1 && (\n209\t \n212\t )}\n213\t
\n214\t );\n215\t}\n216\t\n217\t// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\n218\tfunction Galerie({ images, titre, unitType }:\n219\t { images: string[]; titre: string; unitType?: string }) {\n220\t const [idx, setIdx] = useState(0);\n221\t const [zoom, setZoom] = useState(false);\n222\t const track = useRef(null);\n223\t\n224\t const onScroll = () => {\n225\t const el = track.current;\n226\t if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n227\t };\n228\t const goto = (i: number) =>\n229\t track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n230\t\n231\t if (images.length === 0)\n232\t return (\n233\t
\n234\t \n236\t
\n237\t );\n238\t\n239\t return (\n240\t <>\n241\t
\n242\t
\n243\t {images.map((u, i) => (\n244\t setZoom(true)}\n249\t />\n250\t ))}\n251\t
\n252\t {idx + 1}/{images.length}\n253\t {idx > 0 && (\n254\t \n255\t )}\n256\t {idx < images.length - 1 && (\n257\t \n258\t )}\n259\t
\n260\t {images.length > 1 && (\n261\t
\n262\t {images.map((u, i) => (\n263\t \n267\t ))}\n268\t
\n269\t )}\n270\t {zoom && (\n271\t setZoom(false)} />\n273\t )}\n274\t \n275\t );\n276\t}\n277\t\n278\texport default function ListingPage() {\n279\t const { uid } = useParams<{ uid: string }>();\n280\t const [l, setL] = useState(null);\n281\t const [error, setError] = useState(null);\n282\t\n283\t useEffect(() => {\n284\t fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n285\t if (!uid) return;\n286\t fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n287\t markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n288\t window.scrollTo(0, 0);\n289\t }, [uid]);\n290\t\n291\t if (error)\n292\t return (\n293\t
\n294\t
\n295\t

Annonce introuvable

\n296\t

{error}

\n297\t Retour aux logements\n298\t
\n299\t );\n300\t\n301\t if (!l)\n302\t return (\n303\t
\n304\t
\n305\t
\n306\t
\n307\t
\n308\t
\n309\t );\n310\t\n311\t const dg = l.digest ?? null;\n312\t const f = dg?.faits;\n313\t const conf = dg?.confiance ?? {};\n314\t const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n315\t const confirmes = badgesConfirmes(l);\n316\t const autres = l.amenities.filter(\n317\t (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n318\t const inc = l.details?.inclusions ?? {};\n319\t const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n320\t\n321\t const enLigneDepuis = l.first_seen\n322\t ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n323\t const hist = (l.price_history ?? []).filter((h) => h.price != null);\n324\t const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n325\t ? { de: hist[1].price!, a: hist[0].price! } : null;\n326\t\n327\t const updated = l.updated_at\n328\t ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n329\t day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n330\t\n331\t // chips clés (haute confiance seulement pour les faits extraits du texte)\n332\t const chips: string[] = [];\n333\t if (l.unit_type) chips.push(l.unit_type);\n334\t const dispo = fmtAvailability(l.availability_date);\n335\t if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n336\t if (l.furnished) chips.push(\"Meublé\");\n337\t if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n338\t if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n339\t if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n340\t chips.push(`${f.nb_occupants_total} occupants`);\n341\t if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n342\t chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n343\t if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n344\t\n345\t const pois = l.poi ?? [];\n346\t\n347\t return (\n348\t
\n349\t \n354\t\n355\t
\n356\t {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n357\t ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n358\t
\n359\t
\n360\t \n362\t
\n363\t\n364\t
\n365\t
\n366\t {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n367\t
\n368\t {l.fv_verdict\n369\t ?
\n370\t : deal &&
{deal.txt}
}\n371\t

{l.title || l.address}

\n372\t
\n373\t {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n374\t
\n375\t
\n376\t {chips.map((c) => {c})}\n377\t
\n378\t \n386\t \n388\t Voir l'annonce chez {sourceName(l.source)} ↗\n389\t \n390\t \n392\t Télécharger la fiche (PDF)\n393\t \n394\t
\n395\t\n396\t
\n397\t

Description

\n398\t {dg ? (\n399\t <>\n400\t {dg.en_bref &&

{dg.en_bref}

}\n401\t {dg.sections.map((s) => (\n402\t
\n403\t

{s.titre}

\n404\t

{s.texte}

\n405\t
\n406\t ))}\n407\t
\n408\t Voir le texte original de la source\n409\t

{l.description}

\n410\t
\n411\t \n412\t ) : (\n413\t l.description\n414\t ?

{l.description}

\n415\t :

La source ne fournit pas de description pour cette annonce.

\n416\t )}\n417\t
\n418\t\n419\t
\n420\t

Inclusions et commodités

\n421\t {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n422\t
\n423\t {confirmes.map((b) => (\n424\t ✓ {b}\n425\t ))}\n426\t {autres.map((a) => (\n427\t {a}\n428\t ))}\n429\t
\n430\t {confirmes.length === 0 && autres.length === 0 && (\n431\t

La source ne précise pas les inclusions.

\n432\t )}\n433\t
\n434\t\n435\t
\n436\t

Détails pratiques

\n437\t
\n438\t
Gestionnaire
{sourceName(l.source)}
\n439\t {l.price_label && (\n440\t
Prix affiché
{l.price_label}
\n441\t )}\n442\t {f?.duree_bail_minimale_mois && (\n443\t
Bail minimum
{f.duree_bail_minimale_mois} mois
\n444\t )}\n445\t {enLigneDepuis != null && (\n446\t
En ligne depuis
\n447\t
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n448\t )}\n449\t {updated && (\n450\t
Synchronisé
{updated}
\n451\t )}\n452\t
\n453\t {baissePrix && (\n454\t
\n455\t {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n456\t {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n457\t {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n458\t
\n459\t )}\n460\t
\n461\t
\n462\t\n463\t {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n464\t ------- KA Scores, quartier, proximité ---------------------------- */}\n465\t
\n466\t \n467\t\n468\t \n469\t\n470\t {l.lat != null && l.lng != null && (\n471\t
\n472\t

Emplacement

\n473\t }>\n474\t \n475\t \n476\t

\n477\t Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n478\t Position selon l'adresse géocodée (Adresses Québec).\n479\t

\n480\t
\n481\t )}\n482\t\n483\t \n484\t\n485\t \n486\t\n487\t \n488\t\n489\t \n490\t\n491\t \n492\t\n493\t \n494\t\n495\t {l.kascores && }\n496\t\n497\t
\n498\t {l.quartier ? : null}\n499\t
\n500\t\n501\t
\n502\t {pois.length > 0 && (\n503\t <>\n504\t

À proximité

\n505\t {POI_GROUPES.map((g, gi) => {\n506\t const items = pois.filter((p) => g.cats.includes(p.cat));\n507\t if (items.length === 0) return null;\n508\t return (\n509\t
\n510\t \n511\t {g.icone} {g.titre}\n512\t \n513\t {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n514\t \n515\t \n516\t
    \n517\t {items.map((p) => {\n518\t const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n519\t return (\n520\t
  • \n521\t {meta.icon}\n522\t {p.name}\n523\t {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n524\t
  • \n525\t );\n526\t })}\n527\t
\n528\t
\n529\t );\n530\t })}\n531\t
\n532\t Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n533\t
\n534\t \n535\t )}\n536\t
\n537\t
\n538\t
\n539\t\n540\t
\n541\t {updated && <>Dernière synchronisation : {updated}. }\n542\t Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n543\t renvoie à l'annonce originale.\n544\t
\n545\t\n546\t {/* CTA sticky mobile — toujours visible */}\n547\t
\n548\t {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n549\t \n551\t Voir chez {sourceName(l.source)} ↗\n552\t \n553\t
\n554\t
\n555\t );\n556\t}\n557\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b15daf7a-4bbe-4639-b76e-08cde580cc76","timestamp":"2026-08-24T22:09:50.658Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n// blocs sans `order` passaient devant la galerie sur mobile).\n// -----------------------------------------------------------------------------\nimport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\nimport QuartierBlock from \"../components/QuartierBlock\";\nimport SmartImg from \"../components/SmartImg\";\nimport FairValueBadge from \"../components/FairValueBadge\";\nimport PriceAnalysis from \"../components/PriceAnalysis\";\nimport HistoriqueTAL from \"../components/HistoriqueTAL\";\nimport RegistreLoyers from \"../components/RegistreLoyers\";\nimport RisqueInondation from \"../components/RisqueInondation\";\nimport QualiteAir from \"../components/QualiteAir\";\nimport EssenceProche from \"../components/EssenceProche\";\nimport HydroEstimation from \"../components/HydroEstimation\";\nimport CommercesProches from \"../components/CommercesProches\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";\nimport { markSeen } from \"../search/seen\";\n\n// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\nconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n\n// Icônes et libellés des commodités de proximité (louka/poi.py)\nconst POI_META: Record = {\n epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n ecole: { icon: \"🏫\", label: \"École\" },\n garderie: { icon: \"🧸\", label: \"Garderie\" },\n parc: { icon: \"🌳\", label: \"Parc\" },\n bus: { icon: \"🚌\", label: \"Bus\" },\n metro: { icon: \"🚇\", label: \"Métro\" },\n gym: { icon: \"🏋️\", label: \"Gym\" },\n cafe: { icon: \"☕\", label: \"Café\" },\n clinique: { icon: \"🩺\", label: \"Clinique\" },\n hopital: { icon: \"🏥\", label: \"Hôpital\" },\n bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n};\n\n// Regroupement des POI en catégories repliables\nconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n];\n\n// Badge « prix vs marché » — seuils configurables\nconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n\nconst PETS_LABEL: Record = {\n oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n};\n\nconst NBSP = \" \";\n\n/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\nconst fmtMarche = (m: number): string =>\n `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n\nfunction badgeMarche(price: number | null | undefined,\n loyerSecteur: number | null | undefined) {\n if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n const delta = (price - loyerSecteur) / loyerSecteur;\n const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n if (delta <= SEUILS_MARCHE.bonDeal)\n return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n if (delta <= SEUILS_MARCHE.dansLeMarche)\n return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n}\n\n/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\nfunction badgesConfirmes(l: Listing): string[] {\n const d = l.details ?? {};\n const out: string[] = [];\n const inc = d.inclusions ?? {};\n if (inc.heating) out.push(\"Chauffage inclus\");\n if (inc.electricity) out.push(\"Électricité incluse\");\n if (inc.hot_water) out.push(\"Eau chaude incluse\");\n if (inc.internet) out.push(\"Internet inclus\");\n const app = d.appliances ?? {};\n if (app.dishwasher) out.push(\"Lave-vaisselle\");\n if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n if (app.fridge && app.stove) out.push(\"Électroménagers\");\n if (d.ac) out.push(\"Climatisation\");\n if (d.elevator) out.push(\"Ascenseur\");\n if (d.balcony) out.push(\"Balcon\");\n if (d.pool) out.push(\"Piscine\");\n if (d.gym) out.push(\"Gym\");\n if (d.laundry) out.push(\"Buanderie\");\n if (d.storage) out.push(\"Rangement\");\n if (d.parking?.available)\n out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n if (l.furnished) out.push(\"Meublé\");\n if (d.smoking === false) out.push(\"Non-fumeur\");\n return out;\n}\n\n// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\nfunction Lightbox({ images, start, titre, onClose }:\n { images: string[]; start: number; titre: string; onClose: () => void }) {\n const [idx, setIdx] = useState(start);\n const [scale, setScale] = useState(1);\n const [tx, setTx] = useState(0);\n const [ty, setTy] = useState(0);\n const track = useRef(null);\n const pointers = useRef(new Map());\n const pinch = useRef<{ d: number; scale: number } | null>(null);\n const lastTap = useRef(0);\n\n useEffect(() => { // position initiale + verrou du défilement de la page\n track.current?.scrollTo({ left: start * track.current.clientWidth });\n document.body.style.overflow = \"hidden\";\n const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n window.addEventListener(\"keydown\", onKey);\n return () => {\n document.body.style.overflow = \"\";\n window.removeEventListener(\"keydown\", onKey);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n const onScroll = () => {\n const el = track.current;\n if (el && scale === 1) {\n const i = Math.round(el.scrollLeft / el.clientWidth);\n if (i !== idx) { setIdx(i); resetZoom(); }\n }\n };\n const dist = () => {\n const [a, b] = [...pointers.current.values()];\n return Math.hypot(a.x - b.x, a.y - b.y);\n };\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2)\n pinch.current = { d: dist(), scale };\n if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n const now = Date.now();\n if (now - lastTap.current < 300) {\n if (scale > 1) resetZoom(); else setScale(2.5);\n }\n lastTap.current = now;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2 && pinch.current) {\n const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n setScale(s);\n if (s === 1) { setTx(0); setTy(0); }\n } else if (pointers.current.size === 1 && scale > 1) {\n setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n setTy((v) => v + (e.clientY - prev.y));\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size < 2) pinch.current = null;\n };\n\n return (\n
\n \n \n {idx + 1}/{images.length}\n \n 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n >\n {images.map((u, i) => (\n
\n 1\n ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n : undefined}\n />\n
\n ))}\n
\n {scale === 1 && idx > 0 && (\n \n )}\n {scale === 1 && idx < images.length - 1 && (\n \n )}\n
\n );\n}\n\n// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\nfunction Galerie({ images, titre, unitType }:\n { images: string[]; titre: string; unitType?: string }) {\n const [idx, setIdx] = useState(0);\n const [zoom, setZoom] = useState(false);\n const track = useRef(null);\n\n const onScroll = () => {\n const el = track.current;\n if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n };\n const goto = (i: number) =>\n track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n\n if (images.length === 0)\n return (\n
\n \n
\n );\n\n return (\n <>\n
\n
\n {images.map((u, i) => (\n setZoom(true)}\n />\n ))}\n
\n {idx + 1}/{images.length}\n {idx > 0 && (\n \n )}\n {idx < images.length - 1 && (\n \n )}\n
\n {images.length > 1 && (\n
\n {images.map((u, i) => (\n \n ))}\n
\n )}\n {zoom && (\n setZoom(false)} />\n )}\n \n );\n}\n\nexport default function ListingPage() {\n const { uid } = useParams<{ uid: string }>();\n const [l, setL] = useState(null);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n if (!uid) return;\n fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n window.scrollTo(0, 0);\n }, [uid]);\n\n if (error)\n return (\n
\n
\n

Annonce introuvable

\n

{error}

\n Retour aux logements\n
\n );\n\n if (!l)\n return (\n
\n
\n
\n
\n
\n
\n );\n\n const dg = l.digest ?? null;\n const f = dg?.faits;\n const conf = dg?.confiance ?? {};\n const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n const confirmes = badgesConfirmes(l);\n const autres = l.amenities.filter(\n (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n const inc = l.details?.inclusions ?? {};\n const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n\n const enLigneDepuis = l.first_seen\n ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n const hist = (l.price_history ?? []).filter((h) => h.price != null);\n const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n ? { de: hist[1].price!, a: hist[0].price! } : null;\n\n const updated = l.updated_at\n ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n\n // chips clés (haute confiance seulement pour les faits extraits du texte)\n const chips: string[] = [];\n if (l.unit_type) chips.push(l.unit_type);\n const dispo = fmtAvailability(l.availability_date);\n if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n if (l.furnished) chips.push(\"Meublé\");\n if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n chips.push(`${f.nb_occupants_total} occupants`);\n if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n\n const pois = l.poi ?? [];\n\n return (\n
\n \n\n
\n {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n
\n
\n \n
\n\n
\n
\n {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n
\n {l.fv_verdict\n ?
\n : deal &&
{deal.txt}
}\n

{l.title || l.address}

\n
\n {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n
\n
\n {chips.map((c) => {c})}\n
\n \n \n Voir l'annonce chez {sourceName(l.source)} ↗\n \n \n Télécharger la fiche (PDF)\n \n
\n\n
\n

Description

\n {dg ? (\n <>\n {dg.en_bref &&

{dg.en_bref}

}\n {dg.sections.map((s) => (\n
\n

{s.titre}

\n

{s.texte}

\n
\n ))}\n
\n Voir le texte original de la source\n

{l.description}

\n
\n \n ) : (\n l.description\n ?

{l.description}

\n :

La source ne fournit pas de description pour cette annonce.

\n )}\n
\n\n
\n

Inclusions et commodités

\n {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n
\n {confirmes.map((b) => (\n ✓ {b}\n ))}\n {autres.map((a) => (\n {a}\n ))}\n
\n {confirmes.length === 0 && autres.length === 0 && (\n

La source ne précise pas les inclusions.

\n )}\n
\n\n
\n

Détails pratiques

\n
\n
Gestionnaire
{sourceName(l.source)}
\n {l.price_label && (\n
Prix affiché
{l.price_label}
\n )}\n {f?.duree_bail_minimale_mois && (\n
Bail minimum
{f.duree_bail_minimale_mois} mois
\n )}\n {enLigneDepuis != null && (\n
En ligne depuis
\n
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n )}\n {updated && (\n
Synchronisé
{updated}
\n )}\n
\n {baissePrix && (\n
\n {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n
\n )}\n
\n
\n\n {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n ------- KA Scores, quartier, proximité ---------------------------- */}\n
\n \n\n \n\n {l.lat != null && l.lng != null && (\n
\n

Emplacement

\n }>\n \n \n

\n Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n Position selon l'adresse géocodée (Adresses Québec).\n

\n
\n )}\n\n \n\n \n\n \n\n \n\n \n\n \n\n {l.kascores && }\n\n
\n {l.quartier ? : null}\n
\n\n
\n {pois.length > 0 && (\n <>\n

À proximité

\n {POI_GROUPES.map((g, gi) => {\n const items = pois.filter((p) => g.cats.includes(p.cat));\n if (items.length === 0) return null;\n return (\n
\n \n {g.icone} {g.titre}\n \n {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n \n \n
    \n {items.map((p) => {\n const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n return (\n
  • \n {meta.icon}\n {p.name}\n {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n
  • \n );\n })}\n
\n
\n );\n })}\n
\n Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n
\n \n )}\n
\n
\n
\n\n
\n {updated && <>Dernière synchronisation : {updated}. }\n Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n renvoie à l'annonce originale.\n
\n\n {/* CTA sticky mobile — toujours visible */}\n
\n {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n \n Voir chez {sourceName(l.source)} ↗\n \n
\n
\n );\n}\n","numLines":557,"startLine":1,"totalLines":557}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"a8955b97-359f-4e35-bbef-fe5b81cc71a8","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLZ4hav8HEAyXPUAgjR","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISywMKpwEIERgCKkATeW67TOPgp6xHP3D7xdTtAhlxDvSrtCNr/0zpNP84zMuhC2TPYhRNwiG2bOs0Zf24t26zf5WefnodCkdbTZdZMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGygrPUBrABAhIMYDjbXmfAKKUc5dSUGgziFZ3BdVmP1v6I+mEiMFqWRPOEFI+BfLY4KwsVMHOyD7JiCeDaBWklc84eBaSXF+PNoGhsUbLPU1mKGVYypirQAVvJRKf7vRfLm/I2PcElRBhUisCI+gxV+2AZBe196Cz2pYwm2Ljg88Z+XGVI7d7UiUBF6aX2RaSfcICVzJheF0fTaSLgGkfBLX9q0Yk2Fth3OnsnqK+JBSiXuEpGEHQlYMqUW4ovA8KI1OZOEZbCrF5S5ADws57px/DMrwfUxLn2ne7rA1UEaqj63ZsZfAPSJ34tCASZHzvKDKXG2+ELXOu1XUPaKm3UUp5RwJUnqS0kAxK37IfVYtcmDufIaaylyQFU5XAkSgZqENEeQ18lL6oYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":12060,"cache_read_input_tokens":135802,"cache_creation":{"ephemeral_5m_input_tokens":12060,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"34eadd12-5bba-4de8-b72a-e5e18d90aec7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLZ4hav8HEAyXPUAgjR","type":"message","role":"assistant","content":[{"type":"text","text":"Je regarde l'interface `Listing` dans api.ts avant d'ajouter les types et fetchers."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":12060,"cache_read_input_tokens":135802,"cache_creation":{"ephemeral_5m_input_tokens":12060,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7869ea17-3ed4-4fe5-81bf-2d1802ef93d0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLZ4hav8HEAyXPUAgjR","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013BrUxQBmebUFxUJ1iMwxt6","name":"Grep","input":{"pattern":"export interface Listing","path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","output_mode":"content","-A":60},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":12060,"cache_read_input_tokens":135802,"cache_creation":{"ephemeral_5m_input_tokens":12060,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"791eaea1-1fda-44c9-acd5-14cff3186d34"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013BrUxQBmebUFxUJ1iMwxt6","type":"tool_result","content":"7:export interface ListingDetails {\n8- inclusions?: Record;\n9- appliances?: Record;\n10- parking?: { available?: boolean; type?: string; included?: boolean; price?: number };\n11- contact?: { phone?: string; email?: string };\n12- ac?: boolean;\n13- elevator?: boolean;\n14- balcony?: boolean;\n15- pool?: boolean;\n16- gym?: boolean;\n17- laundry?: boolean;\n18- storage?: boolean;\n19- smoking?: boolean;\n20- floor?: number;\n21- price_from?: boolean;\n22-}\n23-\n24-export interface Poi {\n25- cat: string; // epicerie, pharmacie, ecole, parc, bus…\n26- name: string;\n27- dist_m: number;\n28-}\n29-\n30-export interface Digest {\n31- version: number;\n32- texte_nettoye: string;\n33- en_bref: string | null;\n34- sections: { titre: string; texte: string }[];\n35- faits: {\n36- prix_mensuel: number | null;\n37- date_disponibilite: string | null;\n38- duree_bail_minimale_mois: number | null;\n39- nb_occupants_total: number | null;\n40- salle_de_bain: \"commune\" | \"privee\" | null;\n41- cuisine: \"commune\" | \"privee\" | null;\n42- electromenagers: string[];\n43- inclusions: string[];\n44- contraintes: string[];\n45- depot_mentionne: string | null;\n46- quartier_mentionne: string | null;\n47- };\n48- confiance: Record;\n49- incoherences: string[];\n50- completude: number;\n51-}\n52-\n53-export interface Quartier {\n54- dauid?: string | null;\n55- demographie?: {\n56- population: number | null;\n57- densite: number | null;\n58- age_median: number | null;\n59- revenu_median: number | null;\n60- pct_locataires: number | null;\n61- loyer_moyen: number | null;\n62- pct_francais: number | null;\n63- pct_univ: number | null;\n64- };\n65- proximite?: Record; // scores 0..1 (PMD StatCan)\n66- defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };\n67- chaleur?: { classe: number; ecart: number | null }; // 1 fraîcheur … 9 chaleur\n--\n74:export interface Listing {\n75- uid: string;\n76- source: string;\n77- external_id: string;\n78- url: string;\n79- title: string;\n80- address: string;\n81- sector: string;\n82- city: string;\n83- unit_type: string;\n84- price: number | null;\n85- price_label: string;\n86- availability: string;\n87- availability_date: string | null; // ISO \"2026-07-01\" ou \"now\"\n88- area_sqft: number | null;\n89- pets: string | null; // \"oui\" | \"non\" | \"conditions\"\n90- furnished: boolean | null;\n91- description: string;\n92- amenities: string[];\n93- details: ListingDetails;\n94- images: string[];\n95- lat: number | null;\n96- lng: number | null;\n97- bedrooms: number | null;\n98- poi?: Poi[]; // commodités de proximité (fiche seulement)\n99- quartier?: Quartier | null; // stats de quartier (fiche seulement)\n100- digest?: Digest | null; // description structurée (fiche seulement)\n101- price_history?: { ts: number; price: number | null }[];\n102- first_seen?: number;\n103- last_seen: number;\n104- updated_at: number;\n105- active: number;\n106- // juste valeur locative (fair value) — louka/fairvalue.py\n107- fv?: number | null; // valeur estimée ($/mois)\n108- fv_low?: number | null; // fourchette basse\n109- fv_high?: number | null; // fourchette haute\n110- fv_deviation?: number | null; // (prix - fv) / fv\n111- fv_verdict?: \"sous\" | \"marche\" | \"sur\" | null;\n112- fv_confidence?: \"fort\" | \"moyen\" | \"faible\" | null;\n113- // KA Scores (0-100, null = données insuffisantes / non desservi)\n114- ks_walk?: number | null;\n115- ks_transit?: number | null;\n116- ks_bike?: number | null;\n117- ks_calme?: number | null;\n118- ks_services?: number | null;\n119- ks_global?: number | null;\n120- kascores?: KaScores | null; // détail complet (fiche seulement)\n121-}\n122-\n123-// --- KA Scores — famille de scores maison Lou-Ka (louka/kascores.py) --------\n124-export interface KaScores {\n125- walk: number | null;\n126- transit: number | null;\n127- bike: number | null;\n128- calme: number | null;\n129- services: number | null;\n130- global: number | null;\n131- details: {\n132- walk?: { cats?: { cat: string; dist_m: number | null; pts: number }[]; bonus_choix?: number; raison?: string };\n133- transit?: { arret_bus_m?: number | null; station_metro_m?: number | null; pmd_percentile?: number | null; raison?: string };\n134- bike?: { km_cyclables_1km?: number; note?: string; raison?: string };\n--\n321:export interface ListingFilters {\n322- city?: string;\n323- sector?: string;\n324- unit_type?: string;\n325- source?: string;\n326- price_min?: string;\n327- price_max?: string;\n328- pets?: string; // \"oui\" -> acceptés (oui OU conditions)\n329- furnished?: string; // \"1\" | \"0\"\n330- available_by?: string; // ISO : dispo maintenant ou avant cette date\n331- area_min?: string; // superficie minimale (pi²)\n332- q?: string;\n333- deal?: string; // \"sous\" | \"marche\" | \"sur\" (juste valeur)\n334- kascore_min?: string; // KA Score global minimal (\"60\" | \"70\" | \"80\")\n335- sort?: string; // \"deal\" affaires | \"ka\" KA Score\n336-}\n337-\n338-export function fetchListings(f: ListingFilters, limit?: number, offset?: number) {\n339- const params = new URLSearchParams();\n340- for (const [k, v] of Object.entries(f)) if (v) params.set(k, v);\n341- if (limit != null) params.set(\"limit\", String(limit));\n342- if (offset) params.set(\"offset\", String(offset));\n343- return get<{ total: number; listings: Listing[] }>(`/api/listings?${params}`);\n344-}\n345-\n346-// --- Recherche unifiée liste + carte (/api/search) ---------------------------\n347-/ Une réponse = la page de liste + TOUS les points carte, même tri : le\n348-// compteur de la liste est par construction égal au nombre de marqueurs.\n349-\n350-/** Point compact : [uid, lng, lat, prix, verdict (\"s\"|\"m\"|\"o\"|null)]. */\n351-export type SearchPoint = [string, number, number, number | null, string | null];\n352-\n353-export type SearchSort = \"prix\" | \"prix_desc\" | \"recent\" | \"deal\" | \"ka\";\n354-\n355-export interface SearchQuery extends Omit {\n356- bbox?: string; // ouest,sud,est,nord — zone visible de la carte\n357- poly?: string; // lng,lat;lng,lat;… — zone dessinée\n358- sort?: SearchSort;\n359- page?: number;\n360- page_size?: number;\n361- include?: \"tout\" | \"liste\"; // \"liste\" : points inchangés côté client\n362-}\n363-\n364-export interface SearchResponse {\n365- total: number; // compteur partagé liste = carte\n366- page: number;\n367- page_size: number;\n368- sort: SearchSort;\n369- listings: Listing[];\n370- points: SearchPoint[] | null; // null si include=\"liste\"\n371- unpositioned: number; // annonces filtrées sans coordonnées\n372-}\n373-\n374-export async function fetchSearch(\n375- qy: SearchQuery,\n376- signal?: AbortSignal,\n377-): Promise {\n378- const params = new URLSearchParams();\n379- for (const [k, v] of Object.entries(qy)) if (v) params.set(k, String(v));\n380- const res = await fetch(`/api/search?${params}`, { signal });\n381- if (!res.ok) throw new Error(`API ${res.status} — /api/search`);"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"26a4a22c-8237-42ac-be1b-73f950f301dd","timestamp":"2026-08-24T22:09:56.524Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"7:export interface ListingDetails {\n8- inclusions?: Record;\n9- appliances?: Record;\n10- parking?: { available?: boolean; type?: string; included?: boolean; price?: number };\n11- contact?: { phone?: string; email?: string };\n12- ac?: boolean;\n13- elevator?: boolean;\n14- balcony?: boolean;\n15- pool?: boolean;\n16- gym?: boolean;\n17- laundry?: boolean;\n18- storage?: boolean;\n19- smoking?: boolean;\n20- floor?: number;\n21- price_from?: boolean;\n22-}\n23-\n24-export interface Poi {\n25- cat: string; // epicerie, pharmacie, ecole, parc, bus…\n26- name: string;\n27- dist_m: number;\n28-}\n29-\n30-export interface Digest {\n31- version: number;\n32- texte_nettoye: string;\n33- en_bref: string | null;\n34- sections: { titre: string; texte: string }[];\n35- faits: {\n36- prix_mensuel: number | null;\n37- date_disponibilite: string | null;\n38- duree_bail_minimale_mois: number | null;\n39- nb_occupants_total: number | null;\n40- salle_de_bain: \"commune\" | \"privee\" | null;\n41- cuisine: \"commune\" | \"privee\" | null;\n42- electromenagers: string[];\n43- inclusions: string[];\n44- contraintes: string[];\n45- depot_mentionne: string | null;\n46- quartier_mentionne: string | null;\n47- };\n48- confiance: Record;\n49- incoherences: string[];\n50- completude: number;\n51-}\n52-\n53-export interface Quartier {\n54- dauid?: string | null;\n55- demographie?: {\n56- population: number | null;\n57- densite: number | null;\n58- age_median: number | null;\n59- revenu_median: number | null;\n60- pct_locataires: number | null;\n61- loyer_moyen: number | null;\n62- pct_francais: number | null;\n63- pct_univ: number | null;\n64- };\n65- proximite?: Record; // scores 0..1 (PMD StatCan)\n66- defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };\n67- chaleur?: { classe: number; ecart: number | null }; // 1 fraîcheur … 9 chaleur\n--\n74:export interface Listing {\n75- uid: string;\n76- source: string;\n77- external_id: string;\n78- url: string;\n79- title: string;\n80- address: string;\n81- sector: string;\n82- city: string;\n83- unit_type: string;\n84- price: number | null;\n85- price_label: string;\n86- availability: string;\n87- availability_date: string | null; // ISO \"2026-07-01\" ou \"now\"\n88- area_sqft: number | null;\n89- pets: string | null; // \"oui\" | \"non\" | \"conditions\"\n90- furnished: boolean | null;\n91- description: string;\n92- amenities: string[];\n93- details: ListingDetails;\n94- images: string[];\n95- lat: number | null;\n96- lng: number | null;\n97- bedrooms: number | null;\n98- poi?: Poi[]; // commodités de proximité (fiche seulement)\n99- quartier?: Quartier | null; // stats de quartier (fiche seulement)\n100- digest?: Digest | null; // description structurée (fiche seulement)\n101- price_history?: { ts: number; price: number | null }[];\n102- first_seen?: number;\n103- last_seen: number;\n104- updated_at: number;\n105- active: number;\n106- // juste valeur locative (fair value) — louka/fairvalue.py\n107- fv?: number | null; // valeur estimée ($/mois)\n108- fv_low?: number | null; // fourchette basse\n109- fv_high?: number | null; // fourchette haute\n110- fv_deviation?: number | null; // (prix - fv) / fv\n111- fv_verdict?: \"sous\" | \"marche\" | \"sur\" | null;\n112- fv_confidence?: \"fort\" | \"moyen\" | \"faible\" | null;\n113- // KA Scores (0-100, null = données insuffisantes / non desservi)\n114- ks_walk?: number | null;\n115- ks_transit?: number | null;\n116- ks_bike?: number | null;\n117- ks_calme?: number | null;\n118- ks_services?: number | null;\n119- ks_global?: number | null;\n120- kascores?: KaScores | null; // détail complet (fiche seulement)\n121-}\n122-\n123-// --- KA Scores — famille de scores maison Lou-Ka (louka/kascores.py) --------\n124-export interface KaScores {\n125- walk: number | null;\n126- transit: number | null;\n127- bike: number | null;\n128- calme: number | null;\n129- services: number | null;\n130- global: number | null;\n131- details: {\n132- walk?: { cats?: { cat: string; dist_m: number | null; pts: number }[]; bonus_choix?: number; raison?: string };\n133- transit?: { arret_bus_m?: number | null; station_metro_m?: number | null; pmd_percentile?: number | null; raison?: string };\n134- bike?: { km_cyclables_1km?: number; note?: string; raison?: string };\n--\n321:export interface ListingFilters {\n322- city?: string;\n323- sector?: string;\n324- unit_type?: string;\n325- source?: string;\n326- price_min?: string;\n327- price_max?: string;\n328- pets?: string; // \"oui\" -> acceptés (oui OU conditions)\n329- furnished?: string; // \"1\" | \"0\"\n330- available_by?: string; // ISO : dispo maintenant ou avant cette date\n331- area_min?: string; // superficie minimale (pi²)\n332- q?: string;\n333- deal?: string; // \"sous\" | \"marche\" | \"sur\" (juste valeur)\n334- kascore_min?: string; // KA Score global minimal (\"60\" | \"70\" | \"80\")\n335- sort?: string; // \"deal\" affaires | \"ka\" KA Score\n336-}\n337-\n338-export function fetchListings(f: ListingFilters, limit?: number, offset?: number) {\n339- const params = new URLSearchParams();\n340- for (const [k, v] of Object.entries(f)) if (v) params.set(k, v);\n341- if (limit != null) params.set(\"limit\", String(limit));\n342- if (offset) params.set(\"offset\", String(offset));\n343- return get<{ total: number; listings: Listing[] }>(`/api/listings?${params}`);\n344-}\n345-\n346-// --- Recherche unifiée liste + carte (/api/search) ---------------------------\n347-/ Une réponse = la page de liste + TOUS les points carte, même tri : le\n348-// compteur de la liste est par construction égal au nombre de marqueurs.\n349-\n350-/** Point compact : [uid, lng, lat, prix, verdict (\"s\"|\"m\"|\"o\"|null)]. */\n351-export type SearchPoint = [string, number, number, number | null, string | null];\n352-\n353-export type SearchSort = \"prix\" | \"prix_desc\" | \"recent\" | \"deal\" | \"ka\";\n354-\n355-export interface SearchQuery extends Omit {\n356- bbox?: string; // ouest,sud,est,nord — zone visible de la carte\n357- poly?: string; // lng,lat;lng,lat;… — zone dessinée\n358- sort?: SearchSort;\n359- page?: number;\n360- page_size?: number;\n361- include?: \"tout\" | \"liste\"; // \"liste\" : points inchangés côté client\n362-}\n363-\n364-export interface SearchResponse {\n365- total: number; // compteur partagé liste = carte\n366- page: number;\n367- page_size: number;\n368- sort: SearchSort;\n369- listings: Listing[];\n370- points: SearchPoint[] | null; // null si include=\"liste\"\n371- unpositioned: number; // annonces filtrées sans coordonnées\n372-}\n373-\n374-export async function fetchSearch(\n375- qy: SearchQuery,\n376- signal?: AbortSignal,\n377-): Promise {\n378- const params = new URLSearchParams();\n379- for (const [k, v] of Object.entries(qy)) if (v) params.set(k, String(v));\n380- const res = await fetch(`/api/search?${params}`, { signal });\n381- if (!res.ok) throw new Error(`API ${res.status} — /api/search`);","numLines":185}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"96d408a3-771c-47a5-bdb1-fa461bc55e8e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLZVnx263uPKYHYLixQ","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3330,"cache_read_input_tokens":147862,"cache_creation":{"ephemeral_5m_input_tokens":3330,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8aa9fbd3-a24b-4220-aa65-d852678d5cec"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLZVnx263uPKYHYLixQ","type":"message","role":"assistant","content":[{"type":"text","text":"J'ajoute les types et fetchers dans api.ts."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3330,"cache_read_input_tokens":147862,"cache_creation":{"ephemeral_5m_input_tokens":3330,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b2e8be15-d43c-4d4d-8407-814dc9a4b60e"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLZVnx263uPKYHYLixQ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VkqPnFrvtjAWj5QFB3wmhf","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","old_string":"/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>","new_string":"// --- Dossier d'intelligence locative ----------------------------------------\n// Chaque donnée porte son statut : observed (vu par les crawls), calculated\n// (calcul déterministe), estimated (estimation sourcée), inferred (inférence\n// explicable), unknown (dit tel quel — jamais inventé).\n\nexport type Statut = \"observed\" | \"calculated\" | \"estimated\" | \"inferred\" | \"included\" | \"unknown\";\n\nexport interface CoutReelLigne {\n poste: string;\n statut: Statut;\n montant: number | null;\n source?: string | null;\n note?: string | null;\n}\n\nexport interface CoutReel {\n uid: string;\n loyer: number | null;\n lignes: CoutReelLigne[];\n total_estime: number | null;\n annuel_estime: number | null;\n postes_inconnus: string[];\n total_pi2?: number;\n pi2?: {\n valeur: number;\n percentile_ville?: number; n_ville?: number; portee_ville?: string;\n percentile_secteur?: number; n_secteur?: number;\n percentile_note?: string;\n } | null;\n methode: string;\n}\n\nexport const fetchCoutReel = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/cout-reel`);\n\nexport interface HistoriqueLouka {\n premiere_observation: number | null;\n derniere_observation: number | null;\n active: boolean;\n jours_en_ligne: number;\n prix_initial: number | null;\n prix_actuel: number | null;\n variation: number | null;\n modifications: number;\n timeline: {\n ts: number; type: string; statut: string;\n prix?: number | null; prix_avant?: number | null;\n avant?: unknown; apres?: unknown;\n }[];\n methode: string;\n}\n\nexport const fetchHistorique = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/historique`);\n\nexport interface Recyclees {\n matches: {\n uid: string; source: string; prix: number | null;\n unit_type: string | null;\n derniere_observation: number | null; premiere_observation: number | null;\n confiance: number; signaux: string[];\n }[];\n statut: string;\n methode: string;\n}\n\nexport const fetchRecyclees = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/recyclees`);\n\nexport interface Immeuble {\n bkey: string;\n address: string | null;\n city: string | null;\n computed_at: number;\n annonces_total: number;\n annonces_actives: number;\n annonces_30j: number;\n annonces_90j: number;\n annonces_12m: number;\n unites_identifiees: number;\n unites_estimees: number;\n loyer_median: number | null;\n loyer_median_par_cc: Record | null;\n pi2_median: number | null;\n pression_loyers: {\n variation_12m: number; mediane_12m: number; mediane_12_24m: number;\n n_12m: number; n_12_24m: number; statut: string;\n } | null;\n rotation: {\n statut: string; classe?: string; ratio?: number;\n annonces_12m?: number; unites_estimees?: number;\n observation_jours?: number; methode?: string;\n };\n gestionnaires: string[];\n sources: Record;\n premiere_observation: number;\n derniere_observation: number;\n unites_actives: { uid: string; unit_type: string | null; price: number | null; bedrooms: number | null }[];\n}\n\nexport interface Hiver {\n score: number;\n classe: string;\n detail: { critere: string; score: number; distance_m: number | null; minutes?: number; nom?: string | null; note?: string }[];\n statut: string;\n methode: string;\n}\n\nexport interface Gestionnaire {\n source_id: string;\n nom: string | null;\n site_web: string | null;\n telephone: string | null;\n annonces_actives: number;\n google_maps?: {\n statut: string;\n nom?: string; adresse?: string; note?: number | null;\n nombre_avis?: number | null; confiance_association?: number;\n signaux?: Record;\n methode?: string; note_methode?: string;\n };\n avis?: {\n n: number; moyenne?: number; distribution?: Record;\n pct_negatif?: number; pct_positif?: number;\n moyenne_12m?: number; n_12m?: number;\n tendance?: string; tendance_delta?: number;\n avec_reponse_proprietaire?: number;\n themes?: Record;\n plaintes_frequentes?: string[];\n methode?: string;\n } | null;\n avis_recents?: {\n note: number | null; texte: string | null; date: string | null;\n auteur: string | null; reponse_proprietaire: boolean;\n analyse: { topics?: string[]; sentiment?: string; severite?: string };\n }[];\n derniere_synchro?: number | null;\n}\n\nexport const fetchGestionnaire = (sourceId: string) =>\n get(`/api/managers/${encodeURIComponent(sourceId)}`);\n\n/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3330,"cache_read_input_tokens":147862,"cache_creation":{"ephemeral_5m_input_tokens":3330,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"556b0dd7-8deb-4a04-a698-5eae00cd4408"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VkqPnFrvtjAWj5QFB3wmhf","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f2f80c85-e2ea-4c9e-aa49-e6251c447113","timestamp":"2026-08-24T22:10:36.012Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","oldString":"/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>","newString":"// --- Dossier d'intelligence locative ----------------------------------------\n// Chaque donnée porte son statut : observed (vu par les crawls), calculated\n// (calcul déterministe), estimated (estimation sourcée), inferred (inférence\n// explicable), unknown (dit tel quel — jamais inventé).\n\nexport type Statut = \"observed\" | \"calculated\" | \"estimated\" | \"inferred\" | \"included\" | \"unknown\";\n\nexport interface CoutReelLigne {\n poste: string;\n statut: Statut;\n montant: number | null;\n source?: string | null;\n note?: string | null;\n}\n\nexport interface CoutReel {\n uid: string;\n loyer: number | null;\n lignes: CoutReelLigne[];\n total_estime: number | null;\n annuel_estime: number | null;\n postes_inconnus: string[];\n total_pi2?: number;\n pi2?: {\n valeur: number;\n percentile_ville?: number; n_ville?: number; portee_ville?: string;\n percentile_secteur?: number; n_secteur?: number;\n percentile_note?: string;\n } | null;\n methode: string;\n}\n\nexport const fetchCoutReel = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/cout-reel`);\n\nexport interface HistoriqueLouka {\n premiere_observation: number | null;\n derniere_observation: number | null;\n active: boolean;\n jours_en_ligne: number;\n prix_initial: number | null;\n prix_actuel: number | null;\n variation: number | null;\n modifications: number;\n timeline: {\n ts: number; type: string; statut: string;\n prix?: number | null; prix_avant?: number | null;\n avant?: unknown; apres?: unknown;\n }[];\n methode: string;\n}\n\nexport const fetchHistorique = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/historique`);\n\nexport interface Recyclees {\n matches: {\n uid: string; source: string; prix: number | null;\n unit_type: string | null;\n derniere_observation: number | null; premiere_observation: number | null;\n confiance: number; signaux: string[];\n }[];\n statut: string;\n methode: string;\n}\n\nexport const fetchRecyclees = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/recyclees`);\n\nexport interface Immeuble {\n bkey: string;\n address: string | null;\n city: string | null;\n computed_at: number;\n annonces_total: number;\n annonces_actives: number;\n annonces_30j: number;\n annonces_90j: number;\n annonces_12m: number;\n unites_identifiees: number;\n unites_estimees: number;\n loyer_median: number | null;\n loyer_median_par_cc: Record | null;\n pi2_median: number | null;\n pression_loyers: {\n variation_12m: number; mediane_12m: number; mediane_12_24m: number;\n n_12m: number; n_12_24m: number; statut: string;\n } | null;\n rotation: {\n statut: string; classe?: string; ratio?: number;\n annonces_12m?: number; unites_estimees?: number;\n observation_jours?: number; methode?: string;\n };\n gestionnaires: string[];\n sources: Record;\n premiere_observation: number;\n derniere_observation: number;\n unites_actives: { uid: string; unit_type: string | null; price: number | null; bedrooms: number | null }[];\n}\n\nexport interface Hiver {\n score: number;\n classe: string;\n detail: { critere: string; score: number; distance_m: number | null; minutes?: number; nom?: string | null; note?: string }[];\n statut: string;\n methode: string;\n}\n\nexport interface Gestionnaire {\n source_id: string;\n nom: string | null;\n site_web: string | null;\n telephone: string | null;\n annonces_actives: number;\n google_maps?: {\n statut: string;\n nom?: string; adresse?: string; note?: number | null;\n nombre_avis?: number | null; confiance_association?: number;\n signaux?: Record;\n methode?: string; note_methode?: string;\n };\n avis?: {\n n: number; moyenne?: number; distribution?: Record;\n pct_negatif?: number; pct_positif?: number;\n moyenne_12m?: number; n_12m?: number;\n tendance?: string; tendance_delta?: number;\n avec_reponse_proprietaire?: number;\n themes?: Record;\n plaintes_frequentes?: string[];\n methode?: string;\n } | null;\n avis_recents?: {\n note: number | null; texte: string | null; date: string | null;\n auteur: string | null; reponse_proprietaire: boolean;\n analyse: { topics?: string[]; sentiment?: string; severite?: string };\n }[];\n derniere_synchro?: number | null;\n}\n\nexport const fetchGestionnaire = (sourceId: string) =>\n get(`/api/managers/${encodeURIComponent(sourceId)}`);\n\n/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>","originalFile":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// api.ts : types + client API robuste (timeout, erreurs typées)\n// -----------------------------------------------------------------------------\n\nexport interface ListingDetails {\n inclusions?: Record;\n appliances?: Record;\n parking?: { available?: boolean; type?: string; included?: boolean; price?: number };\n contact?: { phone?: string; email?: string };\n ac?: boolean;\n elevator?: boolean;\n balcony?: boolean;\n pool?: boolean;\n gym?: boolean;\n laundry?: boolean;\n storage?: boolean;\n smoking?: boolean;\n floor?: number;\n price_from?: boolean;\n}\n\nexport interface Poi {\n cat: string; // epicerie, pharmacie, ecole, parc, bus…\n name: string;\n dist_m: number;\n}\n\nexport interface Digest {\n version: number;\n texte_nettoye: string;\n en_bref: string | null;\n sections: { titre: string; texte: string }[];\n faits: {\n prix_mensuel: number | null;\n date_disponibilite: string | null;\n duree_bail_minimale_mois: number | null;\n nb_occupants_total: number | null;\n salle_de_bain: \"commune\" | \"privee\" | null;\n cuisine: \"commune\" | \"privee\" | null;\n electromenagers: string[];\n inclusions: string[];\n contraintes: string[];\n depot_mentionne: string | null;\n quartier_mentionne: string | null;\n };\n confiance: Record;\n incoherences: string[];\n completude: number;\n}\n\nexport interface Quartier {\n dauid?: string | null;\n demographie?: {\n population: number | null;\n densite: number | null;\n age_median: number | null;\n revenu_median: number | null;\n pct_locataires: number | null;\n loyer_moyen: number | null;\n pct_francais: number | null;\n pct_univ: number | null;\n };\n proximite?: Record; // scores 0..1 (PMD StatCan)\n defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };\n chaleur?: { classe: number; ecart: number | null }; // 1 fraîcheur … 9 chaleur\n crime?:\n | { type: \"points\"; rayon_m: number; douze_mois: number; douze_mois_precedents: number;\n categories?: { nom: string; n: number; n_prec: number }[] }\n | { type: \"igc\"; ville: string; annee: number; indice: number; indice_canada: number | null };\n}\n\nexport interface Listing {\n uid: string;\n source: string;\n external_id: string;\n url: string;\n title: string;\n address: string;\n sector: string;\n city: string;\n unit_type: string;\n price: number | null;\n price_label: string;\n availability: string;\n availability_date: string | null; // ISO \"2026-07-01\" ou \"now\"\n area_sqft: number | null;\n pets: string | null; // \"oui\" | \"non\" | \"conditions\"\n furnished: boolean | null;\n description: string;\n amenities: string[];\n details: ListingDetails;\n images: string[];\n lat: number | null;\n lng: number | null;\n bedrooms: number | null;\n poi?: Poi[]; // commodités de proximité (fiche seulement)\n quartier?: Quartier | null; // stats de quartier (fiche seulement)\n digest?: Digest | null; // description structurée (fiche seulement)\n price_history?: { ts: number; price: number | null }[];\n first_seen?: number;\n last_seen: number;\n updated_at: number;\n active: number;\n // juste valeur locative (fair value) — louka/fairvalue.py\n fv?: number | null; // valeur estimée ($/mois)\n fv_low?: number | null; // fourchette basse\n fv_high?: number | null; // fourchette haute\n fv_deviation?: number | null; // (prix - fv) / fv\n fv_verdict?: \"sous\" | \"marche\" | \"sur\" | null;\n fv_confidence?: \"fort\" | \"moyen\" | \"faible\" | null;\n // KA Scores (0-100, null = données insuffisantes / non desservi)\n ks_walk?: number | null;\n ks_transit?: number | null;\n ks_bike?: number | null;\n ks_calme?: number | null;\n ks_services?: number | null;\n ks_global?: number | null;\n kascores?: KaScores | null; // détail complet (fiche seulement)\n}\n\n// --- KA Scores — famille de scores maison Lou-Ka (louka/kascores.py) --------\nexport interface KaScores {\n walk: number | null;\n transit: number | null;\n bike: number | null;\n calme: number | null;\n services: number | null;\n global: number | null;\n details: {\n walk?: { cats?: { cat: string; dist_m: number | null; pts: number }[]; bonus_choix?: number; raison?: string };\n transit?: { arret_bus_m?: number | null; station_metro_m?: number | null; pmd_percentile?: number | null; raison?: string };\n bike?: { km_cyclables_1km?: number; note?: string; raison?: string };\n calme?: { sources_bruit?: { source: string; dist_m: number | null; pen: number }[]; bonus_parc?: number; note?: string };\n services?: { familles?: Record; raison?: string };\n labels?: Record;\n };\n version: string;\n computed_at: number;\n}\n\nexport interface KaScoresStats {\n annonces: number;\n avec_score: number;\n couverture_pct: number;\n version: string;\n moyennes: Record;\n}\n\nexport const fetchKaScoresStats = () => get(\"/api/kascores/stats\");\n\n/** Libellé d'un KA Score (mêmes seuils que louka/kascores.py). */\nexport function kaLabel(score: number | null | undefined): string | null {\n if (score == null) return null;\n if (score >= 85) return \"Exceptionnel\";\n if (score >= 70) return \"Excellent\";\n if (score >= 55) return \"Très bon\";\n if (score >= 40) return \"Moyen\";\n return \"Faible\";\n}\n\n/** Pondérations personnalisées du KA Score global (localStorage). */\nexport const KA_DEFAULT_WEIGHTS: Record = {\n walk: 0.30, transit: 0.20, bike: 0.15, calme: 0.20, services: 0.15,\n};\n\nexport function kaWeights(): Record {\n try {\n const raw = localStorage.getItem(\"louka_ks_poids\");\n if (!raw) return KA_DEFAULT_WEIGHTS;\n const w = JSON.parse(raw) as Record;\n return Object.keys(KA_DEFAULT_WEIGHTS).every((k) => typeof w[k] === \"number\")\n ? w : KA_DEFAULT_WEIGHTS;\n } catch {\n return KA_DEFAULT_WEIGHTS;\n }\n}\n\n/** KA Score global recalculé avec les pondérations de l'utilisateur. */\nexport function kaGlobal(\n s: { walk?: number | null; transit?: number | null; bike?: number | null;\n calme?: number | null; services?: number | null },\n weights: Record = kaWeights(),\n): number | null {\n let poids = 0, acquis = 0;\n for (const k of Object.keys(KA_DEFAULT_WEIGHTS)) {\n const v = (s as Record)[k];\n if (v != null) { poids += weights[k]!; acquis += weights[k]! * v; }\n }\n return poids < 0.5 ? null : Math.round((acquis / poids) * 10) / 10;\n}\n\nexport interface FairValueDetail {\n uid: string;\n fv: number; fv_low: number; fv_high: number;\n deviation: number | null;\n verdict: \"sous\" | \"marche\" | \"sur\" | null;\n confidence: \"fort\" | \"moyen\" | \"faible\";\n method: string; comps: number; segment: string;\n model_version: string;\n segment_n: number;\n histogram: { x0: number; x1: number; n: number }[];\n}\n\nexport const fetchFairValue = (uid: string) =>\n get(`/api/fairvalue/${encodeURIComponent(uid)}`);\n\nexport interface InondationZone {\n type: string; severite: \"eleve\" | \"modere\" | \"present\";\n recurrence: string; distance_m: number; date_rapport: string | null;\n}\n\nexport interface Inondation {\n statut: \"en_zone\" | \"a_proximite\" | \"hors_zone\" | \"non_cartographie\";\n severite: \"eleve\" | \"modere\" | \"present\" | null;\n couvert: boolean;\n zones: InondationZone[];\n}\n\n/** Risque d'inondation BDZI (gouv. du Québec) au point de l'annonce. */\nexport const fetchInondation = (lat: number, lng: number) =>\n get(`/api/inondation?lat=${lat}&lng=${lng}`);\n\nexport interface RdlItem {\n address: string; city: string | null; price: number; rooms: number | null;\n year: number | null; date: string | null; dist_m: number;\n heating: boolean; furnished: boolean;\n}\n\nexport interface RdlNearby {\n n: number; radius_m: number; median?: number;\n median_recent?: number | null; n_recent?: number;\n by_rooms?: Record;\n quartiles?: { p10: number; p25: number; p75: number; p90: number } | null;\n items: RdlItem[];\n}\n\n/** Loyers déclarés au Registre des loyers autour d'un point (fiche). */\nexport const fetchRdl = (lat: number, lng: number, radius = 600) =>\n get(`/api/rdl?lat=${lat}&lng=${lng}&radius=${radius}`);\n\nexport interface TalDecision {\n date: string | null;\n citation: string | null;\n demandeur: \"locateur\" | \"locataire\" | null;\n tags: string[];\n verdict: \"accueillie\" | \"rejetée\" | null;\n url: string;\n}\n\nexport interface TalHistory {\n status: \"ok\" | \"pending\" | \"error\" | \"na\";\n checked_at?: string;\n n?: number;\n contre_locataire?: number;\n eviction?: number;\n last_date?: string | null;\n decisions?: TalDecision[];\n}\n\n/** Décisions du Tribunal administratif du logement (SOQUIJ) à l'adresse. */\nexport const fetchTal = (address: string, city?: string | null) =>\n get(`/api/tal?address=${encodeURIComponent(address)}` +\n (city ? `&city=${encodeURIComponent(city)}` : \"\"));\n\n/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>\n m < 1000 ? `${Math.round(m / 10) * 10} m` : `${(m / 1000).toFixed(1).replace(\".\", \",\")} km`;\n\nexport interface Facets {\n cities: string[];\n sectors: string[];\n unit_types: string[];\n sources: { source: string; n: number }[];\n}\n\nexport interface Source {\n id: string;\n name: string;\n url: string;\n listing_url: string;\n sectors: string;\n connector: string | null;\n status: string;\n active_listings: number;\n last_sync: number | null;\n}\n\nexport interface Stats {\n total: number;\n quebec: number;\n levis: number;\n montreal: number;\n autres: number; // reste de la province (Outaouais, Estrie, Mauricie…)\n sources: number;\n avg_price: number | null;\n}\n\nconst SOURCE_NAMES: Record = {};\n\nexport function registerSourceNames(sources: Source[]) {\n for (const s of sources) SOURCE_NAMES[s.id] = s.name;\n}\nexport function sourceName(id: string): string {\n return SOURCE_NAMES[id] ?? id;\n}\n\nasync function get(path: string): Promise {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), 20000);\n try {\n const res = await fetch(path, { signal: ctrl.signal });\n if (!res.ok) throw new Error(`API ${res.status} — ${path}`);\n return (await res.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface ListingFilters {\n city?: string;\n sector?: string;\n unit_type?: string;\n source?: string;\n price_min?: string;\n price_max?: string;\n pets?: string; // \"oui\" -> acceptés (oui OU conditions)\n furnished?: string; // \"1\" | \"0\"\n available_by?: string; // ISO : dispo maintenant ou avant cette date\n area_min?: string; // superficie minimale (pi²)\n q?: string;\n deal?: string; // \"sous\" | \"marche\" | \"sur\" (juste valeur)\n kascore_min?: string; // KA Score global minimal (\"60\" | \"70\" | \"80\")\n sort?: string; // \"deal\" affaires | \"ka\" KA Score\n}\n\nexport function fetchListings(f: ListingFilters, limit?: number, offset?: number) {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(f)) if (v) params.set(k, v);\n if (limit != null) params.set(\"limit\", String(limit));\n if (offset) params.set(\"offset\", String(offset));\n return get<{ total: number; listings: Listing[] }>(`/api/listings?${params}`);\n}\n\n// --- Recherche unifiée liste + carte (/api/search) ---------------------------\n// Une réponse = la page de liste + TOUS les points carte, même tri : le\n// compteur de la liste est par construction égal au nombre de marqueurs.\n\n/** Point compact : [uid, lng, lat, prix, verdict (\"s\"|\"m\"|\"o\"|null)]. */\nexport type SearchPoint = [string, number, number, number | null, string | null];\n\nexport type SearchSort = \"prix\" | \"prix_desc\" | \"recent\" | \"deal\" | \"ka\";\n\nexport interface SearchQuery extends Omit {\n bbox?: string; // ouest,sud,est,nord — zone visible de la carte\n poly?: string; // lng,lat;lng,lat;… — zone dessinée\n sort?: SearchSort;\n page?: number;\n page_size?: number;\n include?: \"tout\" | \"liste\"; // \"liste\" : points inchangés côté client\n}\n\nexport interface SearchResponse {\n total: number; // compteur partagé liste = carte\n page: number;\n page_size: number;\n sort: SearchSort;\n listings: Listing[];\n points: SearchPoint[] | null; // null si include=\"liste\"\n unpositioned: number; // annonces filtrées sans coordonnées\n}\n\nexport async function fetchSearch(\n qy: SearchQuery,\n signal?: AbortSignal,\n): Promise {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(qy)) if (v) params.set(k, String(v));\n const res = await fetch(`/api/search?${params}`, { signal });\n if (!res.ok) throw new Error(`API ${res.status} — /api/search`);\n return (await res.json()) as SearchResponse;\n}\n\nexport interface GroupStat {\n key: string;\n count: number;\n sources?: number;\n avg_price: number | null;\n min_price: number | null;\n}\n\nexport interface DetailedStats {\n totals: {\n total: number;\n with_price: number;\n avg: number | null;\n median: number | null;\n min: number | null;\n max: number | null;\n sources: number;\n cities: number;\n regions: number;\n gps_pct: number | null;\n superficie_moyenne: number | null;\n dispo_now: number;\n };\n histogram: { lo: number; hi: number | null; count: number }[];\n by_type: GroupStat[];\n by_city: GroupStat[];\n by_source: GroupStat[];\n by_region: GroupStat[];\n offre: {\n furnished_pct: number | null;\n pets_oui_pct: number | null;\n pets_connu: number;\n chauffage_pct: number | null;\n electricite_pct: number | null;\n eau_chaude_pct: number | null;\n internet_pct: number | null;\n clim_pct: number | null;\n stationnement_pct: number | null;\n balcon_pct: number | null;\n dispo_now: number;\n dispo_date: number;\n dispo_inconnue: number;\n superficie_moyenne: number | null;\n superficie_connue: number;\n prix_pi2: { key: string; count: number; val: number }[];\n };\n baisses: { uid: string; title: string; city: string; avant: number; apres: number; pct: number }[];\n sante: { sources_sync_24h: number; alertes_24h: { source: string; message: string; ts: number }[] };\n}\n\nexport const fetchDetailedStats = () => get(\"/api/stats/detailed\");\n\nexport const fetchListing = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}`);\nexport const fetchFacets = (city?: string) =>\n get(`/api/facets${city ? `?city=${encodeURIComponent(city)}` : \"\"}`);\n\n/** Date ISO à +n jours (pour « dispo d'ici 1 mois », etc.) */\nexport function isoInDays(n: number): string {\n const d = new Date();\n d.setDate(d.getDate() + n);\n return d.toISOString().slice(0, 10);\n}\nexport const fetchSources = () => get<{ sources: Source[] }>(\"/api/sources\");\nexport const fetchStats = () => get(\"/api/stats\");\n\n// -- compte utilisateur (connexion Google) ------------------------------------\nexport type Socials = Partial>;\n\nexport interface Me {\n uid: number;\n ka_id: string; // identifiant membre « ka-0123456789 » (écosystème Groupe KA)\n email: string;\n name: string; // nom effectif (display_name sinon nom Google)\n google_name: string;\n picture: string; // avatar effectif (photo téléversée sinon Google)\n google_picture: string;\n avatar_url: string | null; // photo téléversée (« /uploads/avatars/… »)\n display_name: string;\n bio: string;\n city: string;\n phone: string;\n website: string;\n socials: Socials;\n public: boolean; // profil public /u/{ka_id} activé (opt-in)\n role: \"locataire\" | \"gestionnaire\" | null;\n org_source: string | null; // source réclamée (gestionnaire)\n created_at: number | null;\n last_login: number | null;\n provider: string;\n // — profil géré au HUB Groupe KA (source de vérité : groupe-ka.com/compte) —\n profile_source?: \"groupe-ka\" | \"local\";\n role_label?: string; // statut Groupe KA (ex. « ÉQUIPE · GROUPE KA »)\n job_title?: string;\n company?: string;\n age?: number | null;\n public_url?: string; // page publique du hub (si profil public)\n}\n\nexport const setRole = (role: \"locataire\" | \"gestionnaire\") =>\n fetch(`/api/me/role?role=${role}`, { method: \"POST\" });\n\nexport const updateMyProfile = (fields: {\n display_name: string; bio: string; city: string;\n phone: string; website: string; socials: Socials;\n}) => fetch(\"/api/me/profile\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(fields),\n});\n\nexport async function uploadAvatar(file: File): Promise {\n const fd = new FormData();\n fd.append(\"file\", file);\n const res = await fetch(\"/api/me/avatar\", { method: \"POST\", body: fd });\n if (!res.ok) return null;\n return ((await res.json()) as { avatar_url: string }).avatar_url;\n}\nexport const deleteAvatar = () => fetch(\"/api/me/avatar\", { method: \"DELETE\" });\n\n// -- favoris (locataire) -------------------------------------------------------\nexport const fetchFavorites = () =>\n get<{ uids: string[]; listings: Listing[] }>(\"/api/favorites\");\nexport const addFavorite = (uid: string) =>\n fetch(`/api/favorites/${encodeURIComponent(uid)}`, { method: \"POST\" });\nexport const removeFavorite = (uid: string) =>\n fetch(`/api/favorites/${encodeURIComponent(uid)}`, { method: \"DELETE\" });\n\n// -- page gestionnaire ---------------------------------------------------------\nexport interface OrgProfile {\n source: string;\n name: string;\n active_listings: number;\n claimed?: boolean;\n tagline?: string;\n description?: string;\n website?: string;\n phone?: string;\n email?: string;\n logo_url?: string;\n}\nexport const fetchMyOrg = () =>\n get<{ source: string | null; name?: string; active_listings?: number;\n profile?: Record }>(\"/api/org/mine\");\nexport const claimOrg = (source: string) =>\n fetch(`/api/org/claim?source=${encodeURIComponent(source)}`, { method: \"POST\" });\nexport const updateOrg = (fields: {\n tagline: string; description: string; website: string;\n phone: string; email: string; logo_url: string;\n}) => fetch(\"/api/org\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(fields),\n});\nexport const fetchOrg = (sourceId: string) =>\n get(`/api/org/${encodeURIComponent(sourceId)}`);\n\nexport interface PublicProfile {\n ka_id: string;\n name: string;\n picture: string;\n bio: string;\n city: string;\n website: string;\n socials: Socials;\n role: \"locataire\" | \"gestionnaire\" | null;\n created_at: number | null;\n // — champs du HUB Groupe KA (profile_source === \"groupe-ka\") —\n profile_source?: \"groupe-ka\" | \"local\";\n role_label?: string;\n job_title?: string;\n company?: string;\n age?: number | null;\n}\nexport const fetchPublicProfile = (kaId: string) =>\n get(`/api/users/${encodeURIComponent(kaId)}`);\nexport const setPublicProfile = (enabled: boolean) =>\n fetch(`/api/me/public?enabled=${enabled}`, { method: \"POST\" });\n/** Profil connecté, ou null (401 = simplement pas connecté). */\nexport async function fetchMe(): Promise {\n try {\n const res = await fetch(\"/api/me\");\n return res.ok ? ((await res.json()) as Me) : null;\n } catch {\n return null;\n }\n}\nexport const fetchAuthConfig = () => get<{ google: boolean; ka: boolean }>(\"/api/auth/config\");\nexport const logout = () => fetch(\"/api/auth/logout\", { method: \"POST\" });\n\nexport const fmtPrice = (p: number | null, label?: string) =>\n p != null\n ? p.toLocaleString(\"fr-CA\", { maximumFractionDigits: 0 }) + \" $\"\n : label || \"Prix sur demande\";\n\n/** \"now\" -> « Maintenant », \"2026-12-01\" -> « 1ᵉʳ décembre 2026 » */\nexport function fmtAvailability(iso: string | null): string | null {\n if (!iso) return null;\n if (iso === \"now\") return \"Maintenant\";\n const [y, m, d] = iso.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n const txt = new Date(y, m - 1, d).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\",\n });\n return txt.replace(/^1 /, \"1ᵉʳ \");\n}\n\n// --- Images optimisées + visuel de secours Lou-Ka ---------------------------\n/** Miniature WebP servie à la taille de l'écran (voir /api/img côté API).\n * Si l'API ne connaît pas encore l'URL (audit en cours), elle répond 404 et\n * le composant retombe sur l'URL originale via onError. */\nexport const thumb = (u: string, w: 160 | 480 | 800 | 1280 = 480) =>\n `/api/img?u=${encodeURIComponent(u)}&w=${w}`;\n\n/** Visuel de secours élégant aux couleurs Lou-Ka (jamais d'icône d'image\n * cassée) — SVG inline avec le type d'unité en étiquette. */\nexport function placeholderImage(label?: string): string {\n const txt = (label || \"Logement\").slice(0, 12);\n const svg =\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `${txt}` +\n `Photo à venir · Lou-Ka` +\n ``;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n}\n\nexport interface AirMesure {\n moyenne: number; annee: number; n: number; unite: string;\n ref: number | null; ref_nom: string | null;\n}\n\nexport interface AirNearby {\n station: string | null; ville?: string; distance_km?: number;\n mesures: Record;\n}\n\n/** Qualité de l'air : station RSQAQ la plus proche (MELCCFP). */\nexport const fetchAir = (lat: number, lng: number) =>\n get(`/api/air?lat=${lat}&lng=${lng}`);\n\nexport interface GazStation {\n nom: string; adresse: string; dist_m: number;\n regulier: number | null; super: number | null; diesel: number | null;\n moins_chere: boolean;\n}\n\nexport interface GazNearby {\n n: number; rayon_m: number; mediane_regulier: number | null;\n min_regulier: number | null; stations: GazStation[]; maj: string | null;\n}\n\n/** Stations-service à proximité et prix courants (gazquebec.ca). */\nexport const fetchGaz = (lat: number, lng: number) =>\n get(`/api/gaz?lat=${lat}&lng=${lng}`);\n\nexport interface CommerceItem {\n id: string; commerce: string; nom: string; adresse: string;\n dist_m: number; lat: number; lng: number;\n}\n\nexport interface CommercesNearby {\n n: number; commerces: CommerceItem[]; transit?: CommerceItem[];\n}\n\n/** Grands commerces + métro/bus les plus proches (Mapbox / OSM). */\nexport const fetchCommerces = (lat: number, lng: number) =>\n get(`/api/commerces?lat=${lat}&lng=${lng}`);\n\nexport interface HydroEstimate {\n disponible: boolean; raison?: string; adresse?: string;\n cout_annuel?: number; cout_mensuel?: number; kwh_annuel?: number; cache?: boolean; en_attente?: boolean;\n}\n\n/** Coût d'électricité estimé à l'adresse (outil public Hydro-Québec). */\nexport const fetchHydro = (\n adresse: string,\n opts: { uid?: string; lat?: number | null; lng?: number | null } = {},\n solve = false,\n) => {\n const q = new URLSearchParams({ adresse });\n if (opts.uid) q.set(\"uid\", opts.uid);\n if (opts.lat != null) q.set(\"lat\", String(opts.lat));\n if (opts.lng != null) q.set(\"lng\", String(opts.lng));\n if (solve) q.set(\"solve\", \"1\");\n return get(`/api/hydro?${q.toString()}`);\n};\n","structuredPatch":[{"oldStart":264,"oldLines":6,"newStart":264,"newLines":148,"lines":[" get(`/api/tal?address=${encodeURIComponent(address)}` +"," (city ? `&city=${encodeURIComponent(city)}` : \"\"));"," ","+// --- Dossier d'intelligence locative ----------------------------------------","+// Chaque donnée porte son statut : observed (vu par les crawls), calculated","+// (calcul déterministe), estimated (estimation sourcée), inferred (inférence","+// explicable), unknown (dit tel quel — jamais inventé).","+","+export type Statut = \"observed\" | \"calculated\" | \"estimated\" | \"inferred\" | \"included\" | \"unknown\";","+","+export interface CoutReelLigne {","+ poste: string;","+ statut: Statut;","+ montant: number | null;","+ source?: string | null;","+ note?: string | null;","+}","+","+export interface CoutReel {","+ uid: string;","+ loyer: number | null;","+ lignes: CoutReelLigne[];","+ total_estime: number | null;","+ annuel_estime: number | null;","+ postes_inconnus: string[];","+ total_pi2?: number;","+ pi2?: {","+ valeur: number;","+ percentile_ville?: number; n_ville?: number; portee_ville?: string;","+ percentile_secteur?: number; n_secteur?: number;","+ percentile_note?: string;","+ } | null;","+ methode: string;","+}","+","+export const fetchCoutReel = (uid: string) =>","+ get(`/api/listings/${encodeURIComponent(uid)}/cout-reel`);","+","+export interface HistoriqueLouka {","+ premiere_observation: number | null;","+ derniere_observation: number | null;","+ active: boolean;","+ jours_en_ligne: number;","+ prix_initial: number | null;","+ prix_actuel: number | null;","+ variation: number | null;","+ modifications: number;","+ timeline: {","+ ts: number; type: string; statut: string;","+ prix?: number | null; prix_avant?: number | null;","+ avant?: unknown; apres?: unknown;","+ }[];","+ methode: string;","+}","+","+export const fetchHistorique = (uid: string) =>","+ get(`/api/listings/${encodeURIComponent(uid)}/historique`);","+","+export interface Recyclees {","+ matches: {","+ uid: string; source: string; prix: number | null;","+ unit_type: string | null;","+ derniere_observation: number | null; premiere_observation: number | null;","+ confiance: number; signaux: string[];","+ }[];","+ statut: string;","+ methode: string;","+}","+","+export const fetchRecyclees = (uid: string) =>","+ get(`/api/listings/${encodeURIComponent(uid)}/recyclees`);","+","+export interface Immeuble {","+ bkey: string;","+ address: string | null;","+ city: string | null;","+ computed_at: number;","+ annonces_total: number;","+ annonces_actives: number;","+ annonces_30j: number;","+ annonces_90j: number;","+ annonces_12m: number;","+ unites_identifiees: number;","+ unites_estimees: number;","+ loyer_median: number | null;","+ loyer_median_par_cc: Record | null;","+ pi2_median: number | null;","+ pression_loyers: {","+ variation_12m: number; mediane_12m: number; mediane_12_24m: number;","+ n_12m: number; n_12_24m: number; statut: string;","+ } | null;","+ rotation: {","+ statut: string; classe?: string; ratio?: number;","+ annonces_12m?: number; unites_estimees?: number;","+ observation_jours?: number; methode?: string;","+ };","+ gestionnaires: string[];","+ sources: Record;","+ premiere_observation: number;","+ derniere_observation: number;","+ unites_actives: { uid: string; unit_type: string | null; price: number | null; bedrooms: number | null }[];","+}","+","+export interface Hiver {","+ score: number;","+ classe: string;","+ detail: { critere: string; score: number; distance_m: number | null; minutes?: number; nom?: string | null; note?: string }[];","+ statut: string;","+ methode: string;","+}","+","+export interface Gestionnaire {","+ source_id: string;","+ nom: string | null;","+ site_web: string | null;","+ telephone: string | null;","+ annonces_actives: number;","+ google_maps?: {","+ statut: string;","+ nom?: string; adresse?: string; note?: number | null;","+ nombre_avis?: number | null; confiance_association?: number;","+ signaux?: Record;","+ methode?: string; note_methode?: string;","+ };","+ avis?: {","+ n: number; moyenne?: number; distribution?: Record;","+ pct_negatif?: number; pct_positif?: number;","+ moyenne_12m?: number; n_12m?: number;","+ tendance?: string; tendance_delta?: number;","+ avec_reponse_proprietaire?: number;","+ themes?: Record;","+ plaintes_frequentes?: string[];","+ methode?: string;","+ } | null;","+ avis_recents?: {","+ note: number | null; texte: string | null; date: string | null;","+ auteur: string | null; reponse_proprietaire: boolean;","+ analyse: { topics?: string[]; sentiment?: string; severite?: string };","+ }[];","+ derniere_synchro?: number | null;","+}","+","+export const fetchGestionnaire = (sourceId: string) =>","+ get(`/api/managers/${encodeURIComponent(sourceId)}`);","+"," /** 250 -> « 250 m », 1240 -> « 1,2 km » */"," export const fmtDist = (m: number): string =>"," m < 1000 ? `${Math.round(m / 10) * 10} m` : `${(m / 1000).toFixed(1).replace(\".\", \",\")} km`;"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"db27749b-e353-441c-b6af-740f25a8405c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLcQtc5NUpaKBFKEe6N","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01J2Eeqs2bZK8s7d7411vgvX","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","old_string":" ks_global?: number | null;\n kascores?: KaScores | null; // détail complet (fiche seulement)\n}","new_string":" ks_global?: number | null;\n kascores?: KaScores | null; // détail complet (fiche seulement)\n // dossier d'intelligence locative (fiche seulement)\n building_key?: string | null;\n immeuble?: Immeuble | null; // passeport de l'immeuble (précalculé)\n hiver?: Hiver | null; // vie quotidienne en hiver (0-100)\n}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3430,"cache_read_input_tokens":151192,"cache_creation":{"ephemeral_5m_input_tokens":3430,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"99332223-3232-4a9e-9e84-7ed6fdd146d0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01J2Eeqs2bZK8s7d7411vgvX","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"262a11a4-c86f-44ba-869e-693b7e747602","timestamp":"2026-08-24T22:10:41.803Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/api.ts","oldString":" ks_global?: number | null;\n kascores?: KaScores | null; // détail complet (fiche seulement)\n}","newString":" ks_global?: number | null;\n kascores?: KaScores | null; // détail complet (fiche seulement)\n // dossier d'intelligence locative (fiche seulement)\n building_key?: string | null;\n immeuble?: Immeuble | null; // passeport de l'immeuble (précalculé)\n hiver?: Hiver | null; // vie quotidienne en hiver (0-100)\n}","originalFile":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// api.ts : types + client API robuste (timeout, erreurs typées)\n// -----------------------------------------------------------------------------\n\nexport interface ListingDetails {\n inclusions?: Record;\n appliances?: Record;\n parking?: { available?: boolean; type?: string; included?: boolean; price?: number };\n contact?: { phone?: string; email?: string };\n ac?: boolean;\n elevator?: boolean;\n balcony?: boolean;\n pool?: boolean;\n gym?: boolean;\n laundry?: boolean;\n storage?: boolean;\n smoking?: boolean;\n floor?: number;\n price_from?: boolean;\n}\n\nexport interface Poi {\n cat: string; // epicerie, pharmacie, ecole, parc, bus…\n name: string;\n dist_m: number;\n}\n\nexport interface Digest {\n version: number;\n texte_nettoye: string;\n en_bref: string | null;\n sections: { titre: string; texte: string }[];\n faits: {\n prix_mensuel: number | null;\n date_disponibilite: string | null;\n duree_bail_minimale_mois: number | null;\n nb_occupants_total: number | null;\n salle_de_bain: \"commune\" | \"privee\" | null;\n cuisine: \"commune\" | \"privee\" | null;\n electromenagers: string[];\n inclusions: string[];\n contraintes: string[];\n depot_mentionne: string | null;\n quartier_mentionne: string | null;\n };\n confiance: Record;\n incoherences: string[];\n completude: number;\n}\n\nexport interface Quartier {\n dauid?: string | null;\n demographie?: {\n population: number | null;\n densite: number | null;\n age_median: number | null;\n revenu_median: number | null;\n pct_locataires: number | null;\n loyer_moyen: number | null;\n pct_francais: number | null;\n pct_univ: number | null;\n };\n proximite?: Record; // scores 0..1 (PMD StatCan)\n defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };\n chaleur?: { classe: number; ecart: number | null }; // 1 fraîcheur … 9 chaleur\n crime?:\n | { type: \"points\"; rayon_m: number; douze_mois: number; douze_mois_precedents: number;\n categories?: { nom: string; n: number; n_prec: number }[] }\n | { type: \"igc\"; ville: string; annee: number; indice: number; indice_canada: number | null };\n}\n\nexport interface Listing {\n uid: string;\n source: string;\n external_id: string;\n url: string;\n title: string;\n address: string;\n sector: string;\n city: string;\n unit_type: string;\n price: number | null;\n price_label: string;\n availability: string;\n availability_date: string | null; // ISO \"2026-07-01\" ou \"now\"\n area_sqft: number | null;\n pets: string | null; // \"oui\" | \"non\" | \"conditions\"\n furnished: boolean | null;\n description: string;\n amenities: string[];\n details: ListingDetails;\n images: string[];\n lat: number | null;\n lng: number | null;\n bedrooms: number | null;\n poi?: Poi[]; // commodités de proximité (fiche seulement)\n quartier?: Quartier | null; // stats de quartier (fiche seulement)\n digest?: Digest | null; // description structurée (fiche seulement)\n price_history?: { ts: number; price: number | null }[];\n first_seen?: number;\n last_seen: number;\n updated_at: number;\n active: number;\n // juste valeur locative (fair value) — louka/fairvalue.py\n fv?: number | null; // valeur estimée ($/mois)\n fv_low?: number | null; // fourchette basse\n fv_high?: number | null; // fourchette haute\n fv_deviation?: number | null; // (prix - fv) / fv\n fv_verdict?: \"sous\" | \"marche\" | \"sur\" | null;\n fv_confidence?: \"fort\" | \"moyen\" | \"faible\" | null;\n // KA Scores (0-100, null = données insuffisantes / non desservi)\n ks_walk?: number | null;\n ks_transit?: number | null;\n ks_bike?: number | null;\n ks_calme?: number | null;\n ks_services?: number | null;\n ks_global?: number | null;\n kascores?: KaScores | null; // détail complet (fiche seulement)\n}\n\n// --- KA Scores — famille de scores maison Lou-Ka (louka/kascores.py) --------\nexport interface KaScores {\n walk: number | null;\n transit: number | null;\n bike: number | null;\n calme: number | null;\n services: number | null;\n global: number | null;\n details: {\n walk?: { cats?: { cat: string; dist_m: number | null; pts: number }[]; bonus_choix?: number; raison?: string };\n transit?: { arret_bus_m?: number | null; station_metro_m?: number | null; pmd_percentile?: number | null; raison?: string };\n bike?: { km_cyclables_1km?: number; note?: string; raison?: string };\n calme?: { sources_bruit?: { source: string; dist_m: number | null; pen: number }[]; bonus_parc?: number; note?: string };\n services?: { familles?: Record; raison?: string };\n labels?: Record;\n };\n version: string;\n computed_at: number;\n}\n\nexport interface KaScoresStats {\n annonces: number;\n avec_score: number;\n couverture_pct: number;\n version: string;\n moyennes: Record;\n}\n\nexport const fetchKaScoresStats = () => get(\"/api/kascores/stats\");\n\n/** Libellé d'un KA Score (mêmes seuils que louka/kascores.py). */\nexport function kaLabel(score: number | null | undefined): string | null {\n if (score == null) return null;\n if (score >= 85) return \"Exceptionnel\";\n if (score >= 70) return \"Excellent\";\n if (score >= 55) return \"Très bon\";\n if (score >= 40) return \"Moyen\";\n return \"Faible\";\n}\n\n/** Pondérations personnalisées du KA Score global (localStorage). */\nexport const KA_DEFAULT_WEIGHTS: Record = {\n walk: 0.30, transit: 0.20, bike: 0.15, calme: 0.20, services: 0.15,\n};\n\nexport function kaWeights(): Record {\n try {\n const raw = localStorage.getItem(\"louka_ks_poids\");\n if (!raw) return KA_DEFAULT_WEIGHTS;\n const w = JSON.parse(raw) as Record;\n return Object.keys(KA_DEFAULT_WEIGHTS).every((k) => typeof w[k] === \"number\")\n ? w : KA_DEFAULT_WEIGHTS;\n } catch {\n return KA_DEFAULT_WEIGHTS;\n }\n}\n\n/** KA Score global recalculé avec les pondérations de l'utilisateur. */\nexport function kaGlobal(\n s: { walk?: number | null; transit?: number | null; bike?: number | null;\n calme?: number | null; services?: number | null },\n weights: Record = kaWeights(),\n): number | null {\n let poids = 0, acquis = 0;\n for (const k of Object.keys(KA_DEFAULT_WEIGHTS)) {\n const v = (s as Record)[k];\n if (v != null) { poids += weights[k]!; acquis += weights[k]! * v; }\n }\n return poids < 0.5 ? null : Math.round((acquis / poids) * 10) / 10;\n}\n\nexport interface FairValueDetail {\n uid: string;\n fv: number; fv_low: number; fv_high: number;\n deviation: number | null;\n verdict: \"sous\" | \"marche\" | \"sur\" | null;\n confidence: \"fort\" | \"moyen\" | \"faible\";\n method: string; comps: number; segment: string;\n model_version: string;\n segment_n: number;\n histogram: { x0: number; x1: number; n: number }[];\n}\n\nexport const fetchFairValue = (uid: string) =>\n get(`/api/fairvalue/${encodeURIComponent(uid)}`);\n\nexport interface InondationZone {\n type: string; severite: \"eleve\" | \"modere\" | \"present\";\n recurrence: string; distance_m: number; date_rapport: string | null;\n}\n\nexport interface Inondation {\n statut: \"en_zone\" | \"a_proximite\" | \"hors_zone\" | \"non_cartographie\";\n severite: \"eleve\" | \"modere\" | \"present\" | null;\n couvert: boolean;\n zones: InondationZone[];\n}\n\n/** Risque d'inondation BDZI (gouv. du Québec) au point de l'annonce. */\nexport const fetchInondation = (lat: number, lng: number) =>\n get(`/api/inondation?lat=${lat}&lng=${lng}`);\n\nexport interface RdlItem {\n address: string; city: string | null; price: number; rooms: number | null;\n year: number | null; date: string | null; dist_m: number;\n heating: boolean; furnished: boolean;\n}\n\nexport interface RdlNearby {\n n: number; radius_m: number; median?: number;\n median_recent?: number | null; n_recent?: number;\n by_rooms?: Record;\n quartiles?: { p10: number; p25: number; p75: number; p90: number } | null;\n items: RdlItem[];\n}\n\n/** Loyers déclarés au Registre des loyers autour d'un point (fiche). */\nexport const fetchRdl = (lat: number, lng: number, radius = 600) =>\n get(`/api/rdl?lat=${lat}&lng=${lng}&radius=${radius}`);\n\nexport interface TalDecision {\n date: string | null;\n citation: string | null;\n demandeur: \"locateur\" | \"locataire\" | null;\n tags: string[];\n verdict: \"accueillie\" | \"rejetée\" | null;\n url: string;\n}\n\nexport interface TalHistory {\n status: \"ok\" | \"pending\" | \"error\" | \"na\";\n checked_at?: string;\n n?: number;\n contre_locataire?: number;\n eviction?: number;\n last_date?: string | null;\n decisions?: TalDecision[];\n}\n\n/** Décisions du Tribunal administratif du logement (SOQUIJ) à l'adresse. */\nexport const fetchTal = (address: string, city?: string | null) =>\n get(`/api/tal?address=${encodeURIComponent(address)}` +\n (city ? `&city=${encodeURIComponent(city)}` : \"\"));\n\n// --- Dossier d'intelligence locative ----------------------------------------\n// Chaque donnée porte son statut : observed (vu par les crawls), calculated\n// (calcul déterministe), estimated (estimation sourcée), inferred (inférence\n// explicable), unknown (dit tel quel — jamais inventé).\n\nexport type Statut = \"observed\" | \"calculated\" | \"estimated\" | \"inferred\" | \"included\" | \"unknown\";\n\nexport interface CoutReelLigne {\n poste: string;\n statut: Statut;\n montant: number | null;\n source?: string | null;\n note?: string | null;\n}\n\nexport interface CoutReel {\n uid: string;\n loyer: number | null;\n lignes: CoutReelLigne[];\n total_estime: number | null;\n annuel_estime: number | null;\n postes_inconnus: string[];\n total_pi2?: number;\n pi2?: {\n valeur: number;\n percentile_ville?: number; n_ville?: number; portee_ville?: string;\n percentile_secteur?: number; n_secteur?: number;\n percentile_note?: string;\n } | null;\n methode: string;\n}\n\nexport const fetchCoutReel = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/cout-reel`);\n\nexport interface HistoriqueLouka {\n premiere_observation: number | null;\n derniere_observation: number | null;\n active: boolean;\n jours_en_ligne: number;\n prix_initial: number | null;\n prix_actuel: number | null;\n variation: number | null;\n modifications: number;\n timeline: {\n ts: number; type: string; statut: string;\n prix?: number | null; prix_avant?: number | null;\n avant?: unknown; apres?: unknown;\n }[];\n methode: string;\n}\n\nexport const fetchHistorique = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/historique`);\n\nexport interface Recyclees {\n matches: {\n uid: string; source: string; prix: number | null;\n unit_type: string | null;\n derniere_observation: number | null; premiere_observation: number | null;\n confiance: number; signaux: string[];\n }[];\n statut: string;\n methode: string;\n}\n\nexport const fetchRecyclees = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}/recyclees`);\n\nexport interface Immeuble {\n bkey: string;\n address: string | null;\n city: string | null;\n computed_at: number;\n annonces_total: number;\n annonces_actives: number;\n annonces_30j: number;\n annonces_90j: number;\n annonces_12m: number;\n unites_identifiees: number;\n unites_estimees: number;\n loyer_median: number | null;\n loyer_median_par_cc: Record | null;\n pi2_median: number | null;\n pression_loyers: {\n variation_12m: number; mediane_12m: number; mediane_12_24m: number;\n n_12m: number; n_12_24m: number; statut: string;\n } | null;\n rotation: {\n statut: string; classe?: string; ratio?: number;\n annonces_12m?: number; unites_estimees?: number;\n observation_jours?: number; methode?: string;\n };\n gestionnaires: string[];\n sources: Record;\n premiere_observation: number;\n derniere_observation: number;\n unites_actives: { uid: string; unit_type: string | null; price: number | null; bedrooms: number | null }[];\n}\n\nexport interface Hiver {\n score: number;\n classe: string;\n detail: { critere: string; score: number; distance_m: number | null; minutes?: number; nom?: string | null; note?: string }[];\n statut: string;\n methode: string;\n}\n\nexport interface Gestionnaire {\n source_id: string;\n nom: string | null;\n site_web: string | null;\n telephone: string | null;\n annonces_actives: number;\n google_maps?: {\n statut: string;\n nom?: string; adresse?: string; note?: number | null;\n nombre_avis?: number | null; confiance_association?: number;\n signaux?: Record;\n methode?: string; note_methode?: string;\n };\n avis?: {\n n: number; moyenne?: number; distribution?: Record;\n pct_negatif?: number; pct_positif?: number;\n moyenne_12m?: number; n_12m?: number;\n tendance?: string; tendance_delta?: number;\n avec_reponse_proprietaire?: number;\n themes?: Record;\n plaintes_frequentes?: string[];\n methode?: string;\n } | null;\n avis_recents?: {\n note: number | null; texte: string | null; date: string | null;\n auteur: string | null; reponse_proprietaire: boolean;\n analyse: { topics?: string[]; sentiment?: string; severite?: string };\n }[];\n derniere_synchro?: number | null;\n}\n\nexport const fetchGestionnaire = (sourceId: string) =>\n get(`/api/managers/${encodeURIComponent(sourceId)}`);\n\n/** 250 -> « 250 m », 1240 -> « 1,2 km » */\nexport const fmtDist = (m: number): string =>\n m < 1000 ? `${Math.round(m / 10) * 10} m` : `${(m / 1000).toFixed(1).replace(\".\", \",\")} km`;\n\nexport interface Facets {\n cities: string[];\n sectors: string[];\n unit_types: string[];\n sources: { source: string; n: number }[];\n}\n\nexport interface Source {\n id: string;\n name: string;\n url: string;\n listing_url: string;\n sectors: string;\n connector: string | null;\n status: string;\n active_listings: number;\n last_sync: number | null;\n}\n\nexport interface Stats {\n total: number;\n quebec: number;\n levis: number;\n montreal: number;\n autres: number; // reste de la province (Outaouais, Estrie, Mauricie…)\n sources: number;\n avg_price: number | null;\n}\n\nconst SOURCE_NAMES: Record = {};\n\nexport function registerSourceNames(sources: Source[]) {\n for (const s of sources) SOURCE_NAMES[s.id] = s.name;\n}\nexport function sourceName(id: string): string {\n return SOURCE_NAMES[id] ?? id;\n}\n\nasync function get(path: string): Promise {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), 20000);\n try {\n const res = await fetch(path, { signal: ctrl.signal });\n if (!res.ok) throw new Error(`API ${res.status} — ${path}`);\n return (await res.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface ListingFilters {\n city?: string;\n sector?: string;\n unit_type?: string;\n source?: string;\n price_min?: string;\n price_max?: string;\n pets?: string; // \"oui\" -> acceptés (oui OU conditions)\n furnished?: string; // \"1\" | \"0\"\n available_by?: string; // ISO : dispo maintenant ou avant cette date\n area_min?: string; // superficie minimale (pi²)\n q?: string;\n deal?: string; // \"sous\" | \"marche\" | \"sur\" (juste valeur)\n kascore_min?: string; // KA Score global minimal (\"60\" | \"70\" | \"80\")\n sort?: string; // \"deal\" affaires | \"ka\" KA Score\n}\n\nexport function fetchListings(f: ListingFilters, limit?: number, offset?: number) {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(f)) if (v) params.set(k, v);\n if (limit != null) params.set(\"limit\", String(limit));\n if (offset) params.set(\"offset\", String(offset));\n return get<{ total: number; listings: Listing[] }>(`/api/listings?${params}`);\n}\n\n// --- Recherche unifiée liste + carte (/api/search) ---------------------------\n// Une réponse = la page de liste + TOUS les points carte, même tri : le\n// compteur de la liste est par construction égal au nombre de marqueurs.\n\n/** Point compact : [uid, lng, lat, prix, verdict (\"s\"|\"m\"|\"o\"|null)]. */\nexport type SearchPoint = [string, number, number, number | null, string | null];\n\nexport type SearchSort = \"prix\" | \"prix_desc\" | \"recent\" | \"deal\" | \"ka\";\n\nexport interface SearchQuery extends Omit {\n bbox?: string; // ouest,sud,est,nord — zone visible de la carte\n poly?: string; // lng,lat;lng,lat;… — zone dessinée\n sort?: SearchSort;\n page?: number;\n page_size?: number;\n include?: \"tout\" | \"liste\"; // \"liste\" : points inchangés côté client\n}\n\nexport interface SearchResponse {\n total: number; // compteur partagé liste = carte\n page: number;\n page_size: number;\n sort: SearchSort;\n listings: Listing[];\n points: SearchPoint[] | null; // null si include=\"liste\"\n unpositioned: number; // annonces filtrées sans coordonnées\n}\n\nexport async function fetchSearch(\n qy: SearchQuery,\n signal?: AbortSignal,\n): Promise {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(qy)) if (v) params.set(k, String(v));\n const res = await fetch(`/api/search?${params}`, { signal });\n if (!res.ok) throw new Error(`API ${res.status} — /api/search`);\n return (await res.json()) as SearchResponse;\n}\n\nexport interface GroupStat {\n key: string;\n count: number;\n sources?: number;\n avg_price: number | null;\n min_price: number | null;\n}\n\nexport interface DetailedStats {\n totals: {\n total: number;\n with_price: number;\n avg: number | null;\n median: number | null;\n min: number | null;\n max: number | null;\n sources: number;\n cities: number;\n regions: number;\n gps_pct: number | null;\n superficie_moyenne: number | null;\n dispo_now: number;\n };\n histogram: { lo: number; hi: number | null; count: number }[];\n by_type: GroupStat[];\n by_city: GroupStat[];\n by_source: GroupStat[];\n by_region: GroupStat[];\n offre: {\n furnished_pct: number | null;\n pets_oui_pct: number | null;\n pets_connu: number;\n chauffage_pct: number | null;\n electricite_pct: number | null;\n eau_chaude_pct: number | null;\n internet_pct: number | null;\n clim_pct: number | null;\n stationnement_pct: number | null;\n balcon_pct: number | null;\n dispo_now: number;\n dispo_date: number;\n dispo_inconnue: number;\n superficie_moyenne: number | null;\n superficie_connue: number;\n prix_pi2: { key: string; count: number; val: number }[];\n };\n baisses: { uid: string; title: string; city: string; avant: number; apres: number; pct: number }[];\n sante: { sources_sync_24h: number; alertes_24h: { source: string; message: string; ts: number }[] };\n}\n\nexport const fetchDetailedStats = () => get(\"/api/stats/detailed\");\n\nexport const fetchListing = (uid: string) =>\n get(`/api/listings/${encodeURIComponent(uid)}`);\nexport const fetchFacets = (city?: string) =>\n get(`/api/facets${city ? `?city=${encodeURIComponent(city)}` : \"\"}`);\n\n/** Date ISO à +n jours (pour « dispo d'ici 1 mois », etc.) */\nexport function isoInDays(n: number): string {\n const d = new Date();\n d.setDate(d.getDate() + n);\n return d.toISOString().slice(0, 10);\n}\nexport const fetchSources = () => get<{ sources: Source[] }>(\"/api/sources\");\nexport const fetchStats = () => get(\"/api/stats\");\n\n// -- compte utilisateur (connexion Google) ------------------------------------\nexport type Socials = Partial>;\n\nexport interface Me {\n uid: number;\n ka_id: string; // identifiant membre « ka-0123456789 » (écosystème Groupe KA)\n email: string;\n name: string; // nom effectif (display_name sinon nom Google)\n google_name: string;\n picture: string; // avatar effectif (photo téléversée sinon Google)\n google_picture: string;\n avatar_url: string | null; // photo téléversée (« /uploads/avatars/… »)\n display_name: string;\n bio: string;\n city: string;\n phone: string;\n website: string;\n socials: Socials;\n public: boolean; // profil public /u/{ka_id} activé (opt-in)\n role: \"locataire\" | \"gestionnaire\" | null;\n org_source: string | null; // source réclamée (gestionnaire)\n created_at: number | null;\n last_login: number | null;\n provider: string;\n // — profil géré au HUB Groupe KA (source de vérité : groupe-ka.com/compte) —\n profile_source?: \"groupe-ka\" | \"local\";\n role_label?: string; // statut Groupe KA (ex. « ÉQUIPE · GROUPE KA »)\n job_title?: string;\n company?: string;\n age?: number | null;\n public_url?: string; // page publique du hub (si profil public)\n}\n\nexport const setRole = (role: \"locataire\" | \"gestionnaire\") =>\n fetch(`/api/me/role?role=${role}`, { method: \"POST\" });\n\nexport const updateMyProfile = (fields: {\n display_name: string; bio: string; city: string;\n phone: string; website: string; socials: Socials;\n}) => fetch(\"/api/me/profile\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(fields),\n});\n\nexport async function uploadAvatar(file: File): Promise {\n const fd = new FormData();\n fd.append(\"file\", file);\n const res = await fetch(\"/api/me/avatar\", { method: \"POST\", body: fd });\n if (!res.ok) return null;\n return ((await res.json()) as { avatar_url: string }).avatar_url;\n}\nexport const deleteAvatar = () => fetch(\"/api/me/avatar\", { method: \"DELETE\" });\n\n// -- favoris (locataire) -------------------------------------------------------\nexport const fetchFavorites = () =>\n get<{ uids: string[]; listings: Listing[] }>(\"/api/favorites\");\nexport const addFavorite = (uid: string) =>\n fetch(`/api/favorites/${encodeURIComponent(uid)}`, { method: \"POST\" });\nexport const removeFavorite = (uid: string) =>\n fetch(`/api/favorites/${encodeURIComponent(uid)}`, { method: \"DELETE\" });\n\n// -- page gestionnaire ---------------------------------------------------------\nexport interface OrgProfile {\n source: string;\n name: string;\n active_listings: number;\n claimed?: boolean;\n tagline?: string;\n description?: string;\n website?: string;\n phone?: string;\n email?: string;\n logo_url?: string;\n}\nexport const fetchMyOrg = () =>\n get<{ source: string | null; name?: string; active_listings?: number;\n profile?: Record }>(\"/api/org/mine\");\nexport const claimOrg = (source: string) =>\n fetch(`/api/org/claim?source=${encodeURIComponent(source)}`, { method: \"POST\" });\nexport const updateOrg = (fields: {\n tagline: string; description: string; website: string;\n phone: string; email: string; logo_url: string;\n}) => fetch(\"/api/org\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(fields),\n});\nexport const fetchOrg = (sourceId: string) =>\n get(`/api/org/${encodeURIComponent(sourceId)}`);\n\nexport interface PublicProfile {\n ka_id: string;\n name: string;\n picture: string;\n bio: string;\n city: string;\n website: string;\n socials: Socials;\n role: \"locataire\" | \"gestionnaire\" | null;\n created_at: number | null;\n // — champs du HUB Groupe KA (profile_source === \"groupe-ka\") —\n profile_source?: \"groupe-ka\" | \"local\";\n role_label?: string;\n job_title?: string;\n company?: string;\n age?: number | null;\n}\nexport const fetchPublicProfile = (kaId: string) =>\n get(`/api/users/${encodeURIComponent(kaId)}`);\nexport const setPublicProfile = (enabled: boolean) =>\n fetch(`/api/me/public?enabled=${enabled}`, { method: \"POST\" });\n/** Profil connecté, ou null (401 = simplement pas connecté). */\nexport async function fetchMe(): Promise {\n try {\n const res = await fetch(\"/api/me\");\n return res.ok ? ((await res.json()) as Me) : null;\n } catch {\n return null;\n }\n}\nexport const fetchAuthConfig = () => get<{ google: boolean; ka: boolean }>(\"/api/auth/config\");\nexport const logout = () => fetch(\"/api/auth/logout\", { method: \"POST\" });\n\nexport const fmtPrice = (p: number | null, label?: string) =>\n p != null\n ? p.toLocaleString(\"fr-CA\", { maximumFractionDigits: 0 }) + \" $\"\n : label || \"Prix sur demande\";\n\n/** \"now\" -> « Maintenant », \"2026-12-01\" -> « 1ᵉʳ décembre 2026 » */\nexport function fmtAvailability(iso: string | null): string | null {\n if (!iso) return null;\n if (iso === \"now\") return \"Maintenant\";\n const [y, m, d] = iso.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n const txt = new Date(y, m - 1, d).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\",\n });\n return txt.replace(/^1 /, \"1ᵉʳ \");\n}\n\n// --- Images optimisées + visuel de secours Lou-Ka ---------------------------\n/** Miniature WebP servie à la taille de l'écran (voir /api/img côté API).\n * Si l'API ne connaît pas encore l'URL (audit en cours), elle répond 404 et\n * le composant retombe sur l'URL originale via onError. */\nexport const thumb = (u: string, w: 160 | 480 | 800 | 1280 = 480) =>\n `/api/img?u=${encodeURIComponent(u)}&w=${w}`;\n\n/** Visuel de secours élégant aux couleurs Lou-Ka (jamais d'icône d'image\n * cassée) — SVG inline avec le type d'unité en étiquette. */\nexport function placeholderImage(label?: string): string {\n const txt = (label || \"Logement\").slice(0, 12);\n const svg =\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `` +\n `${txt}` +\n `Photo à venir · Lou-Ka` +\n ``;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n}\n\nexport interface AirMesure {\n moyenne: number; annee: number; n: number; unite: string;\n ref: number | null; ref_nom: string | null;\n}\n\nexport interface AirNearby {\n station: string | null; ville?: string; distance_km?: number;\n mesures: Record;\n}\n\n/** Qualité de l'air : station RSQAQ la plus proche (MELCCFP). */\nexport const fetchAir = (lat: number, lng: number) =>\n get(`/api/air?lat=${lat}&lng=${lng}`);\n\nexport interface GazStation {\n nom: string; adresse: string; dist_m: number;\n regulier: number | null; super: number | null; diesel: number | null;\n moins_chere: boolean;\n}\n\nexport interface GazNearby {\n n: number; rayon_m: number; mediane_regulier: number | null;\n min_regulier: number | null; stations: GazStation[]; maj: string | null;\n}\n\n/** Stations-service à proximité et prix courants (gazquebec.ca). */\nexport const fetchGaz = (lat: number, lng: number) =>\n get(`/api/gaz?lat=${lat}&lng=${lng}`);\n\nexport interface CommerceItem {\n id: string; commerce: string; nom: string; adresse: string;\n dist_m: number; lat: number; lng: number;\n}\n\nexport interface CommercesNearby {\n n: number; commerces: CommerceItem[]; transit?: CommerceItem[];\n}\n\n/** Grands commerces + métro/bus les plus proches (Mapbox / OSM). */\nexport const fetchCommerces = (lat: number, lng: number) =>\n get(`/api/commerces?lat=${lat}&lng=${lng}`);\n\nexport interface HydroEstimate {\n disponible: boolean; raison?: string; adresse?: string;\n cout_annuel?: number; cout_mensuel?: number; kwh_annuel?: number; cache?: boolean; en_attente?: boolean;\n}\n\n/** Coût d'électricité estimé à l'adresse (outil public Hydro-Québec). */\nexport const fetchHydro = (\n adresse: string,\n opts: { uid?: string; lat?: number | null; lng?: number | null } = {},\n solve = false,\n) => {\n const q = new URLSearchParams({ adresse });\n if (opts.uid) q.set(\"uid\", opts.uid);\n if (opts.lat != null) q.set(\"lat\", String(opts.lat));\n if (opts.lng != null) q.set(\"lng\", String(opts.lng));\n if (solve) q.set(\"solve\", \"1\");\n return get(`/api/hydro?${q.toString()}`);\n};\n","structuredPatch":[{"oldStart":118,"oldLines":6,"newStart":118,"newLines":10,"lines":[" ks_services?: number | null;"," ks_global?: number | null;"," kascores?: KaScores | null; // détail complet (fiche seulement)","+ // dossier d'intelligence locative (fiche seulement)","+ building_key?: string | null;","+ immeuble?: Immeuble | null; // passeport de l'immeuble (précalculé)","+ hiver?: Hiver | null; // vie quotidienne en hiver (0-100)"," }"," "," // --- KA Scores — famille de scores maison Lou-Ka (louka/kascores.py) --------"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f441b694-ea9f-4f7f-99b5-cd6addb162e2","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLcqPFYRWGaU5q8i2hm","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS5QMKpwEIERgCKkCxdarnpX8Ubkab6Qn5TC83RjUC7jw+VVVZKqMxopucugnjzmLA6PejcdT7LJIUXC1szuLAaep4Qf6pxJKDTm3BMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHpgrPUBrABAhIMeapXnEUavDbojV+hGgzeftIjcl6QGTphfpAiMKznWyFTOPiKFwTXXGJ2JwrkZoICXDoW6rEGO3crV8jjsMysRGIGtofDmKuQAk18QCrqAQA4rP2y12TaE+4WR0hhWozmJ9cabomQ3fouAOLyBenP0a/n4+WfbBAFYrBAnlsPD2aPQIw0kEU6/ug4/NhIqr7Lj+IGsErytoU4rhNR5oQ8GsPHVjBVsFxXTFzz8OY4jJfAXhkIMzGaiJ7bRiIru7Wf6m7WXZta59F4pa3VoPYjTYLKtUOLU6Kt1efSFK+UJGdZUdEiuDOGnQYE6G3UhjtuEe5yHGsWHnR4MVUYMjhrYuDup0NsiUIPZtzdrP/h9vZChJFB8AENQEyVmBnd0ZSdaM0wRm/r2HJfBrdr90vMY04QxgLDSlX4sRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":378,"cache_read_input_tokens":154622,"cache_creation":{"ephemeral_5m_input_tokens":378,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c7db2aa9-18e5-41fa-90dd-e67085c025bc"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLcqPFYRWGaU5q8i2hm","type":"message","role":"assistant","content":[{"type":"text","text":"Maintenant les composants. Je vérifie les classes CSS réutilisables."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":378,"cache_read_input_tokens":154622,"cache_creation":{"ephemeral_5m_input_tokens":378,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"131b620f-72eb-415f-bb28-787f8d2e07fe"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLcqPFYRWGaU5q8i2hm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Hqr6SWm9iiMpR7WF5Egc6L","name":"Bash","input":{"command":"grep -n \"^\\.zi-\\|^\\.kv\\|^\\.cell\\|\\.f-bloc\\b\\|^\\.fine\\|\\.deal-badge\\|\\.chip-key\" frontend/src/styles.css | head -25","description":"List reusable CSS classes"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":378,"cache_read_input_tokens":154622,"cache_creation":{"ephemeral_5m_input_tokens":378,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"846bef97-1895-4232-9f2f-bbd3aa443673"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Hqr6SWm9iiMpR7WF5Egc6L","type":"tool_result","content":"378:.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n380:.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n381:.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n382:.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n384:.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n904:.f-bloc { min-width: 0; }\n905:.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n906:.f-bloc:empty { display: none; }\n941:.deal-badge {\n957:.chip-key {\n1878:.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n1880:.zi-eleve { background: #fde8e8; color: #a12622; }\n1881:.zi-modere { background: #fdf3e0; color: #8a5a00; }\n1882:.zi-present { background: #fdf3e0; color: #8a5a00; }\n1883:.zi-ok { background: #e7f4ea; color: #1e6b34; }\n1884:.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n1885:.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n1886:.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n2016:.f-bloc:not(.f-galerie):not(.f-hero) {\n2024: .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n2028:.f-bloc h2, .quartier h2 {\n2032:.f-bloc h2::before, .quartier h2::before {\n2037:.f-bloc h2::after, .quartier h2::after {\n2082:.zi-badge {\n2088:.zi-badge::before {","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bf85a0ff-ab0e-42a4-9f92-779cafb97f21","timestamp":"2026-08-24T22:10:50.508Z","tool_use_result":{"stdout":"378:.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n380:.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n381:.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n382:.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n384:.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n904:.f-bloc { min-width: 0; }\n905:.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n906:.f-bloc:empty { display: none; }\n941:.deal-badge {\n957:.chip-key {\n1878:.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n1880:.zi-eleve { background: #fde8e8; color: #a12622; }\n1881:.zi-modere { background: #fdf3e0; color: #8a5a00; }\n1882:.zi-present { background: #fdf3e0; color: #8a5a00; }\n1883:.zi-ok { background: #e7f4ea; color: #1e6b34; }\n1884:.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n1885:.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n1886:.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n2016:.f-bloc:not(.f-galerie):not(.f-hero) {\n2024: .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n2028:.f-bloc h2, .quartier h2 {\n2032:.f-bloc h2::before, .quartier h2::before {\n2037:.f-bloc h2::after, .quartier h2::after {\n2082:.zi-badge {\n2088:.zi-badge::before {","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"d8a81c71-5170-46c0-b484-63761a5bf30d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01F9UGfjHPJeKYre1mfKeUze","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/CoutReel.tsx"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1daf08fb-7530-4502-a412-2a777f22f522","timestamp":"2026-08-24T22:11:11.458Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/CoutReel.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/CoutReel.tsx : bloc « Coût réel mensuel » (fiche)\n// Loyer + frais non inclus, ligne par ligne, chaque poste étiqueté :\n// inclus (bail) / observé (source) / estimé (avec sa source) / inconnu\n// (dit tel quel — jamais chiffré arbitrairement). Prix au pi² avec\n// percentile réellement calculé sur les comparables.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { CoutReel as CoutReelT, fetchCoutReel, fmtPrice } from \"../api\";\n\nconst NBSP = \" \";\n\nconst STATUT_META: Record = {\n included: { label: \"inclus\", cls: \"st-included\" },\n observed: { label: \"observé\", cls: \"st-observed\" },\n estimated: { label: \"estimé\", cls: \"st-estimated\" },\n unknown: { label: \"inconnu\", cls: \"st-unknown\" },\n};\n\nexport default function CoutReel({ uid }: { uid: string }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n fetchCoutReel(uid).then(setD).catch(() => setD(null));\n }, [uid]);\n if (!d || d.loyer == null) return null;\n\n const pi2 = d.pi2;\n return (\n
\n

Coût réel mensuel

\n \n \n {d.lignes.map((li) => {\n const m = STATUT_META[li.statut] ?? STATUT_META.unknown;\n return (\n \n \n \n \n );\n })}\n \n {d.total_estime != null && (\n \n \n \n \n \n {d.annuel_estime != null && (\n \n \n \n \n )}\n \n )}\n
\n {li.poste}\n {m.label}\n \n {li.montant != null && li.montant > 0 && fmtPrice(li.montant)}\n {li.montant === 0 && li.statut === \"included\" && `0${NBSP}$`}\n {li.montant == null && \"—\"}\n
Total estimé≈{NBSP}{fmtPrice(d.total_estime)}{NBSP}/mois
soit sur 12 mois≈{NBSP}{fmtPrice(d.annuel_estime)}
\n {d.postes_inconnus.length > 0 && (\n

\n Postes non chiffrables avec les données publiées :{\" \"}\n {d.postes_inconnus.join(\", \").toLowerCase()} — le total réel peut\n être plus élevé.\n

\n )}\n {pi2 && (\n
\n \n {pi2.valeur.toFixed(2).replace(\".\", \",\")}{NBSP}$/pi²\n \n {pi2.percentile_secteur != null && (\n \n {\" \"}moins cher que {100 - pi2.percentile_secteur}{NBSP}% des{\" \"}\n {pi2.n_secteur} logements comparables du secteur (~2{NBSP}km)\n \n )}\n {pi2.percentile_secteur == null && pi2.percentile_ville != null && (\n \n {\" \"}moins cher que {100 - pi2.percentile_ville}{NBSP}% des{\" \"}\n {pi2.n_ville} comparables ({pi2.portee_ville})\n \n )}\n {pi2.percentile_note && {pi2.percentile_note}}\n
\n )}\n

{d.methode}

\n
\n );\n}\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLdUfLiHDLUZErcjs1F","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01F9UGfjHPJeKYre1mfKeUze","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/CoutReel.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/CoutReel.tsx : bloc « Coût réel mensuel » (fiche)\n// Loyer + frais non inclus, ligne par ligne, chaque poste étiqueté :\n// inclus (bail) / observé (source) / estimé (avec sa source) / inconnu\n// (dit tel quel — jamais chiffré arbitrairement). Prix au pi² avec\n// percentile réellement calculé sur les comparables.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { CoutReel as CoutReelT, fetchCoutReel, fmtPrice } from \"../api\";\n\nconst NBSP = \" \";\n\nconst STATUT_META: Record = {\n included: { label: \"inclus\", cls: \"st-included\" },\n observed: { label: \"observé\", cls: \"st-observed\" },\n estimated: { label: \"estimé\", cls: \"st-estimated\" },\n unknown: { label: \"inconnu\", cls: \"st-unknown\" },\n};\n\nexport default function CoutReel({ uid }: { uid: string }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n fetchCoutReel(uid).then(setD).catch(() => setD(null));\n }, [uid]);\n if (!d || d.loyer == null) return null;\n\n const pi2 = d.pi2;\n return (\n
\n

Coût réel mensuel

\n \n \n {d.lignes.map((li) => {\n const m = STATUT_META[li.statut] ?? STATUT_META.unknown;\n return (\n \n \n \n \n );\n })}\n \n {d.total_estime != null && (\n \n \n \n \n \n {d.annuel_estime != null && (\n \n \n \n \n )}\n \n )}\n
\n {li.poste}\n {m.label}\n \n {li.montant != null && li.montant > 0 && fmtPrice(li.montant)}\n {li.montant === 0 && li.statut === \"included\" && `0${NBSP}$`}\n {li.montant == null && \"—\"}\n
Total estimé≈{NBSP}{fmtPrice(d.total_estime)}{NBSP}/mois
soit sur 12 mois≈{NBSP}{fmtPrice(d.annuel_estime)}
\n {d.postes_inconnus.length > 0 && (\n

\n Postes non chiffrables avec les données publiées :{\" \"}\n {d.postes_inconnus.join(\", \").toLowerCase()} — le total réel peut\n être plus élevé.\n

\n )}\n {pi2 && (\n
\n \n {pi2.valeur.toFixed(2).replace(\".\", \",\")}{NBSP}$/pi²\n \n {pi2.percentile_secteur != null && (\n \n {\" \"}moins cher que {100 - pi2.percentile_secteur}{NBSP}% des{\" \"}\n {pi2.n_secteur} logements comparables du secteur (~2{NBSP}km)\n \n )}\n {pi2.percentile_secteur == null && pi2.percentile_ville != null && (\n \n {\" \"}moins cher que {100 - pi2.percentile_ville}{NBSP}% des{\" \"}\n {pi2.n_ville} comparables ({pi2.portee_ville})\n \n )}\n {pi2.percentile_note && {pi2.percentile_note}}\n
\n )}\n

{d.methode}

\n
\n );\n}\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1634,"cache_read_input_tokens":155000,"cache_creation":{"ephemeral_5m_input_tokens":1634,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"14bca475-7b21-4399-9cf9-352afb21ff8e"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"48a5716b-2a70-4358-9360-f77546bc802e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLf2Ax2ywLEjNpNyDY7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01GnNKrVTVpbhVMGPnkHps93","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueLouka.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/HistoriqueLouka.tsx : bloc « Historique Lou-Ka » (fiche)\n// Timeline des observations RÉELLES des synchronisations : changements de\n// prix, description, superficie, disponibilité, inclusions, photos,\n// retraits/retours. + « Vies antérieures » : annonces recyclées probables\n// du même logement (score multi-signaux, inférence explicable).\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport {\n HistoriqueLouka as HistoT, Recyclees,\n fetchHistorique, fetchRecyclees, fmtPrice,\n} from \"../api\";\n\nconst NBSP = \" \";\n\nconst fmtTs = (ts: number | null | undefined): string =>\n ts\n ? new Date(ts * 1000).toLocaleDateString(\"fr-CA\",\n { day: \"numeric\", month: \"short\", year: \"numeric\" })\n : \"—\";\n\nconst EVENT_LABEL: Record = {\n prix: \"Prix\",\n description: \"Description modifiée\",\n superficie: \"Superficie modifiée\",\n dispo: \"Disponibilité modifiée\",\n inclusions: \"Inclusions modifiées\",\n photos: \"Photos modifiées\",\n disparition: \"Annonce retirée\",\n reapparition: \"Annonce republiée\",\n};\n\nexport default function HistoriqueLouka({ uid }: { uid: string }) {\n const [d, setD] = useState(null);\n const [rec, setRec] = useState(null);\n useEffect(() => {\n setD(null); setRec(null);\n fetchHistorique(uid).then(setD).catch(() => setD(null));\n fetchRecyclees(uid).then(setRec).catch(() => setRec(null));\n }, [uid]);\n if (!d) return null;\n\n const variationPct = d.variation != null\n ? `${d.variation > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(d.variation * 100))}${NBSP}%`\n : null;\n const items = d.timeline.slice(0, 12);\n const matches = rec?.matches ?? [];\n\n return (\n
\n

Historique Lou-Ka

\n
\n
\n
Suivie depuis
\n
{fmtTs(d.premiere_observation)}
\n
\n
\n
En ligne
\n
{d.jours_en_ligne}{NBSP}jour{d.jours_en_ligne > 1 ? \"s\" : \"\"}
\n
\n {d.prix_initial != null && d.prix_actuel != null && d.prix_initial !== d.prix_actuel && (\n
\n
Prix initial → actuel
\n
\n {fmtPrice(d.prix_initial)} → {fmtPrice(d.prix_actuel)}\n {variationPct && ` (${variationPct})`}\n
\n
\n )}\n
\n
Modifications observées
\n
{d.modifications}
\n
\n
\n\n {items.length > 0 && (\n
    \n {items.map((it, i) => (\n
  • \n {fmtTs(it.ts)}\n \n {it.type === \"prix\" ? (\n it.prix_avant != null\n ? <>Prix {it.prix_avant! > (it.prix ?? 0) ? \"baissé\" : \"monté\"} de {fmtPrice(it.prix_avant ?? null)} à {fmtPrice(it.prix ?? null)}\n : <>Premier prix observé : {fmtPrice(it.prix ?? null)}\n ) : (\n EVENT_LABEL[it.type] ?? it.type\n )}\n \n
  • \n ))}\n
\n )}\n {items.length === 0 && (\n

\n Aucune modification observée depuis la première synchronisation de\n cette annonce.\n

\n )}\n\n {matches.length > 0 && (\n
\n

Vies antérieures probables de ce logement

\n {matches.slice(0, 3).map((m) => (\n
\n
\n \n republication probable ({m.confiance}{NBSP}% de confiance)\n {\" \"}\n {m.prix != null && <>affiché {fmtPrice(m.prix)}}\n {m.derniere_observation != null && <> jusqu'en {fmtTs(m.derniere_observation)}}\n
\n
{m.signaux.join(\" · \")}
\n
\n ))}\n

{rec?.methode} — inférence, sans fusion automatique.

\n
\n )}\n\n

{d.methode}

\n
\n );\n}\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1742,"cache_read_input_tokens":156634,"cache_creation":{"ephemeral_5m_input_tokens":1742,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a81f20c9-26e4-4c6d-b342-2db4e2b76770"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01GnNKrVTVpbhVMGPnkHps93","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueLouka.tsx"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"206e2518-27f7-4195-a13c-8369184f975f","timestamp":"2026-08-24T22:11:35.203Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HistoriqueLouka.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/HistoriqueLouka.tsx : bloc « Historique Lou-Ka » (fiche)\n// Timeline des observations RÉELLES des synchronisations : changements de\n// prix, description, superficie, disponibilité, inclusions, photos,\n// retraits/retours. + « Vies antérieures » : annonces recyclées probables\n// du même logement (score multi-signaux, inférence explicable).\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport {\n HistoriqueLouka as HistoT, Recyclees,\n fetchHistorique, fetchRecyclees, fmtPrice,\n} from \"../api\";\n\nconst NBSP = \" \";\n\nconst fmtTs = (ts: number | null | undefined): string =>\n ts\n ? new Date(ts * 1000).toLocaleDateString(\"fr-CA\",\n { day: \"numeric\", month: \"short\", year: \"numeric\" })\n : \"—\";\n\nconst EVENT_LABEL: Record = {\n prix: \"Prix\",\n description: \"Description modifiée\",\n superficie: \"Superficie modifiée\",\n dispo: \"Disponibilité modifiée\",\n inclusions: \"Inclusions modifiées\",\n photos: \"Photos modifiées\",\n disparition: \"Annonce retirée\",\n reapparition: \"Annonce republiée\",\n};\n\nexport default function HistoriqueLouka({ uid }: { uid: string }) {\n const [d, setD] = useState(null);\n const [rec, setRec] = useState(null);\n useEffect(() => {\n setD(null); setRec(null);\n fetchHistorique(uid).then(setD).catch(() => setD(null));\n fetchRecyclees(uid).then(setRec).catch(() => setRec(null));\n }, [uid]);\n if (!d) return null;\n\n const variationPct = d.variation != null\n ? `${d.variation > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(d.variation * 100))}${NBSP}%`\n : null;\n const items = d.timeline.slice(0, 12);\n const matches = rec?.matches ?? [];\n\n return (\n
\n

Historique Lou-Ka

\n
\n
\n
Suivie depuis
\n
{fmtTs(d.premiere_observation)}
\n
\n
\n
En ligne
\n
{d.jours_en_ligne}{NBSP}jour{d.jours_en_ligne > 1 ? \"s\" : \"\"}
\n
\n {d.prix_initial != null && d.prix_actuel != null && d.prix_initial !== d.prix_actuel && (\n
\n
Prix initial → actuel
\n
\n {fmtPrice(d.prix_initial)} → {fmtPrice(d.prix_actuel)}\n {variationPct && ` (${variationPct})`}\n
\n
\n )}\n
\n
Modifications observées
\n
{d.modifications}
\n
\n
\n\n {items.length > 0 && (\n
    \n {items.map((it, i) => (\n
  • \n {fmtTs(it.ts)}\n \n {it.type === \"prix\" ? (\n it.prix_avant != null\n ? <>Prix {it.prix_avant! > (it.prix ?? 0) ? \"baissé\" : \"monté\"} de {fmtPrice(it.prix_avant ?? null)} à {fmtPrice(it.prix ?? null)}\n : <>Premier prix observé : {fmtPrice(it.prix ?? null)}\n ) : (\n EVENT_LABEL[it.type] ?? it.type\n )}\n \n
  • \n ))}\n
\n )}\n {items.length === 0 && (\n

\n Aucune modification observée depuis la première synchronisation de\n cette annonce.\n

\n )}\n\n {matches.length > 0 && (\n
\n

Vies antérieures probables de ce logement

\n {matches.slice(0, 3).map((m) => (\n
\n
\n \n republication probable ({m.confiance}{NBSP}% de confiance)\n {\" \"}\n {m.prix != null && <>affiché {fmtPrice(m.prix)}}\n {m.derniere_observation != null && <> jusqu'en {fmtTs(m.derniere_observation)}}\n
\n
{m.signaux.join(\" · \")}
\n
\n ))}\n

{rec?.methode} — inférence, sans fusion automatique.

\n
\n )}\n\n

{d.methode}

\n
\n );\n}\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"5e8e6c76-a042-4168-89fa-7306de10d2ee","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLgmqnnCESjb4NfnE1r","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01T643BxA97tWGKun7Wo1wev","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/ImmeubleBloc.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/ImmeubleBloc.tsx : bloc « Passeport de l'immeuble » (fiche)\n// Tout est calculé sur ce que Lou-Ka observe réellement (annonces actives\n// ET historiques du même bâtiment) : unités, loyers médians, rotation,\n// pression sur les loyers. Un indicateur sans échantillon suffisant est\n// affiché « données insuffisantes » — jamais inventé.\n// -----------------------------------------------------------------------------\nimport { Immeuble, fmtPrice } from \"../api\";\n\nconst NBSP = \" \";\n\nconst fmtTs = (ts: number | null | undefined): string =>\n ts\n ? new Date(ts * 1000).toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" })\n : \"—\";\n\nconst ROTATION_CLS: Record = {\n \"faible\": \"zi-ok\",\n \"normale\": \"zi-nc\",\n \"élevée\": \"zi-modere\",\n \"très élevée\": \"zi-eleve\",\n};\n\nexport default function ImmeubleBloc({ im }: { im: Immeuble }) {\n // au moins 2 annonces regroupées, sinon le « passeport » n'apporte rien\n if (!im || im.annonces_total < 2) return null;\n const rot = im.rotation;\n const pres = im.pression_loyers;\n const parCc = im.loyer_median_par_cc;\n\n return (\n
\n

Passeport de l'immeuble

\n
\n
\n
Annonces observées
\n
{im.annonces_total} dont {im.annonces_actives} active{im.annonces_actives > 1 ? \"s\" : \"\"}
\n
\n
\n
Unités estimées
\n
≥{NBSP}{im.unites_estimees}
\n
\n {im.loyer_median != null && (\n
\n
Loyer médian (actives)
\n
{fmtPrice(im.loyer_median)}
\n
\n )}\n {im.pi2_median != null && (\n
\n
Médiane $/pi²
\n
{im.pi2_median.toFixed(2).replace(\".\", \",\")}{NBSP}$
\n
\n )}\n
\n\n {parCc && Object.keys(parCc).length > 0 && (\n

\n Par nombre de chambres :{\" \"}\n {Object.entries(parCc)\n .map(([cc, v]) => `${cc}${NBSP}ch. ${fmtPrice(v)}`)\n .join(\" · \")}\n

\n )}\n\n
\n
\n Rotation des logements{\" \"}\n {rot.statut === \"calculated\" ? (\n <>\n \n {rot.classe}\n {\" \"}\n \n {rot.annonces_12m} annonce{(rot.annonces_12m ?? 0) > 1 ? \"s\" : \"\"} sur\n 12 mois pour ≥{NBSP}{rot.unites_estimees} unités\n \n \n ) : (\n données insuffisantes\n )}\n
\n
\n Pression sur les loyers{\" \"}\n {pres ? (\n <>\n 0.05 ? \"zi-modere\" : pres.variation_12m < -0.02 ? \"zi-ok\" : \"zi-nc\"}`}>\n {pres.variation_12m > 0 ? \"+\" : \"−\"}\n {Math.abs(Math.round(pres.variation_12m * 100))}{NBSP}% sur 12 mois\n {\" \"}\n \n médiane des prix d'entrée : {fmtPrice(pres.mediane_12_24m)} →{\" \"}\n {fmtPrice(pres.mediane_12m)} ({pres.n_12_24m} vs {pres.n_12m} annonces)\n \n \n ) : (\n échantillon insuffisant\n )}\n
\n
\n\n

\n Immeuble suivi par Lou-Ka depuis {fmtTs(im.premiere_observation)}\n {rot.statut === \"calculated\" && rot.methode ? ` — ${rot.methode}` : (\n \" — indicateurs calculés uniquement sur les annonces observées par \" +\n \"les synchronisations Lou-Ka (pas un recensement du bâtiment).\"\n )}\n

\n
\n );\n}\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2140,"cache_read_input_tokens":158376,"cache_creation":{"ephemeral_5m_input_tokens":2140,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6f0d85c8-ac1f-4893-b312-cdc554722a6d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01T643BxA97tWGKun7Wo1wev","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/ImmeubleBloc.tsx"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"67be9a79-0155-4ea2-be88-021046422d60","timestamp":"2026-08-24T22:11:58.077Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/ImmeubleBloc.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/ImmeubleBloc.tsx : bloc « Passeport de l'immeuble » (fiche)\n// Tout est calculé sur ce que Lou-Ka observe réellement (annonces actives\n// ET historiques du même bâtiment) : unités, loyers médians, rotation,\n// pression sur les loyers. Un indicateur sans échantillon suffisant est\n// affiché « données insuffisantes » — jamais inventé.\n// -----------------------------------------------------------------------------\nimport { Immeuble, fmtPrice } from \"../api\";\n\nconst NBSP = \" \";\n\nconst fmtTs = (ts: number | null | undefined): string =>\n ts\n ? new Date(ts * 1000).toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" })\n : \"—\";\n\nconst ROTATION_CLS: Record = {\n \"faible\": \"zi-ok\",\n \"normale\": \"zi-nc\",\n \"élevée\": \"zi-modere\",\n \"très élevée\": \"zi-eleve\",\n};\n\nexport default function ImmeubleBloc({ im }: { im: Immeuble }) {\n // au moins 2 annonces regroupées, sinon le « passeport » n'apporte rien\n if (!im || im.annonces_total < 2) return null;\n const rot = im.rotation;\n const pres = im.pression_loyers;\n const parCc = im.loyer_median_par_cc;\n\n return (\n
\n

Passeport de l'immeuble

\n
\n
\n
Annonces observées
\n
{im.annonces_total} dont {im.annonces_actives} active{im.annonces_actives > 1 ? \"s\" : \"\"}
\n
\n
\n
Unités estimées
\n
≥{NBSP}{im.unites_estimees}
\n
\n {im.loyer_median != null && (\n
\n
Loyer médian (actives)
\n
{fmtPrice(im.loyer_median)}
\n
\n )}\n {im.pi2_median != null && (\n
\n
Médiane $/pi²
\n
{im.pi2_median.toFixed(2).replace(\".\", \",\")}{NBSP}$
\n
\n )}\n
\n\n {parCc && Object.keys(parCc).length > 0 && (\n

\n Par nombre de chambres :{\" \"}\n {Object.entries(parCc)\n .map(([cc, v]) => `${cc}${NBSP}ch. ${fmtPrice(v)}`)\n .join(\" · \")}\n

\n )}\n\n
\n
\n Rotation des logements{\" \"}\n {rot.statut === \"calculated\" ? (\n <>\n \n {rot.classe}\n {\" \"}\n \n {rot.annonces_12m} annonce{(rot.annonces_12m ?? 0) > 1 ? \"s\" : \"\"} sur\n 12 mois pour ≥{NBSP}{rot.unites_estimees} unités\n \n \n ) : (\n données insuffisantes\n )}\n
\n
\n Pression sur les loyers{\" \"}\n {pres ? (\n <>\n 0.05 ? \"zi-modere\" : pres.variation_12m < -0.02 ? \"zi-ok\" : \"zi-nc\"}`}>\n {pres.variation_12m > 0 ? \"+\" : \"−\"}\n {Math.abs(Math.round(pres.variation_12m * 100))}{NBSP}% sur 12 mois\n {\" \"}\n \n médiane des prix d'entrée : {fmtPrice(pres.mediane_12_24m)} →{\" \"}\n {fmtPrice(pres.mediane_12m)} ({pres.n_12_24m} vs {pres.n_12m} annonces)\n \n \n ) : (\n échantillon insuffisant\n )}\n
\n
\n\n

\n Immeuble suivi par Lou-Ka depuis {fmtTs(im.premiere_observation)}\n {rot.statut === \"calculated\" && rot.methode ? ` — ${rot.methode}` : (\n \" — indicateurs calculés uniquement sur les annonces observées par \" +\n \"les synchronisations Lou-Ka (pas un recensement du bâtiment).\"\n )}\n

\n
\n );\n}\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"779113db-2160-4db5-85be-37d130ecf5bc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLiTazNSkABsp1CB5TH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019V6LVtmxi8iE8UtFgNfwsq","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/GestionnaireBloc.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/GestionnaireBloc.tsx : bloc « Qui gère ce logement? » (fiche)\n// Fiche du gestionnaire (source directe) + réputation Google : distribution\n// réelle des notes, moyenne récente, tendance et thèmes récurrents calculés\n// sur les avis synchronisés dans NOTRE base (louka/managers.py) — pas\n// seulement la moyenne affichée par Google, et zéro appel API au chargement.\n// La fiche Google n'est montrée que si l'association est confiante.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { Gestionnaire, fetchGestionnaire } from \"../api\";\n\nconst NBSP = \" \";\n\nconst SENT_CLS: Record = {\n \"négatif\": \"zi-eleve\", \"neutre\": \"zi-nc\", \"positif\": \"zi-ok\",\n};\n\nexport default function GestionnaireBloc({ source }: { source: string }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n fetchGestionnaire(source).then(setD).catch(() => setD(null));\n }, [source]);\n // sans fiche Google associée, le bloc « Détails pratiques » suffit\n if (!d || !d.google_maps || d.google_maps.statut === \"non_associe\") return null;\n\n const g = d.google_maps;\n const avis = d.avis;\n const dist = avis?.distribution;\n const total = dist ? Object.values(dist).reduce((a, b) => a + b, 0) : 0;\n const recents = (d.avis_recents ?? []).filter((a) => a.texte).slice(0, 3);\n\n return (\n
\n

Qui gère ce logement?

\n
\n
{d.nom}
\n
\n {d.annonces_actives}{NBSP}annonce{d.annonces_actives > 1 ? \"s\" : \"\"} active{d.annonces_actives > 1 ? \"s\" : \"\"} sur Lou-Ka\n {d.site_web && (\n <>\n {\" · \"}\n site web ↗\n \n )}\n
\n
\n\n {g.note != null && (\n
\n ★ {g.note.toFixed(1).replace(\".\", \",\")}\n \n {g.nombre_avis}{NBSP}avis Google — fiche «{NBSP}{g.nom}{NBSP}»\n \n
\n )}\n\n {avis && avis.n > 0 && (\n <>\n {dist && total > 0 && (\n
\n {[5, 4, 3, 2, 1].map((n) => {\n const c = dist[String(n)] ?? 0;\n return (\n
\n {n}★\n \n = 4 ? \"pos\" : \"\"}`}\n style={{ width: `${Math.round((100 * c) / total)}%` }} />\n \n {c}\n
\n );\n })}\n
\n )}\n
\n {avis.moyenne_12m != null && (\n Moyenne des 12 derniers mois : {avis.moyenne_12m.toFixed(1).replace(\".\", \",\")} ({avis.n_12m} avis)\n )}\n {avis.tendance && (\n \n {avis.tendance}\n \n )}\n
\n {(avis.plaintes_frequentes?.length ?? 0) > 0 && (\n
\n Plaintes récurrentes dans les avis :{\" \"}\n {avis.plaintes_frequentes!.map((t) => (\n {t}\n ))}\n
\n )}\n {recents.length > 0 && (\n
\n Extraits d'avis récents ({recents.length})\n {recents.map((a, i) => (\n
\n \n {a.note != null ? `${a.note}★` : \"—\"}\n {\" \"}\n {a.texte}\n
\n {a.date ? new Date(a.date).toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" }) : \"\"}\n {a.reponse_proprietaire && \" · le gestionnaire a répondu\"}\n
\n
\n ))}\n
\n )}\n \n )}\n\n

\n Fiche Google Maps associée automatiquement (confiance{\" \"}\n {Math.round((g.confiance_association ?? 0) * 100)}{NBSP}% —{\" \"}\n {g.methode}). Statistiques calculées sur les {avis?.n ?? 0} avis\n synchronisés dans la base Lou-Ka\n {g.nombre_avis && avis && avis.n < g.nombre_avis\n ? ` (échantillon des plus récents ; Google en annonce ${g.nombre_avis})`\n : \"\"}. Thèmes détectés par lexique — inférence indicative, pas une\n lecture humaine.\n

\n
\n );\n}\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2032,"cache_read_input_tokens":160516,"cache_creation":{"ephemeral_5m_input_tokens":2032,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"79c5e0fd-df80-470e-a74f-1c620e16e5f7"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019V6LVtmxi8iE8UtFgNfwsq","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/GestionnaireBloc.tsx"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"45bec53a-a304-4670-ad8e-24b2a7ef2ea2","timestamp":"2026-08-24T22:12:22.910Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/GestionnaireBloc.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/GestionnaireBloc.tsx : bloc « Qui gère ce logement? » (fiche)\n// Fiche du gestionnaire (source directe) + réputation Google : distribution\n// réelle des notes, moyenne récente, tendance et thèmes récurrents calculés\n// sur les avis synchronisés dans NOTRE base (louka/managers.py) — pas\n// seulement la moyenne affichée par Google, et zéro appel API au chargement.\n// La fiche Google n'est montrée que si l'association est confiante.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { Gestionnaire, fetchGestionnaire } from \"../api\";\n\nconst NBSP = \" \";\n\nconst SENT_CLS: Record = {\n \"négatif\": \"zi-eleve\", \"neutre\": \"zi-nc\", \"positif\": \"zi-ok\",\n};\n\nexport default function GestionnaireBloc({ source }: { source: string }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n fetchGestionnaire(source).then(setD).catch(() => setD(null));\n }, [source]);\n // sans fiche Google associée, le bloc « Détails pratiques » suffit\n if (!d || !d.google_maps || d.google_maps.statut === \"non_associe\") return null;\n\n const g = d.google_maps;\n const avis = d.avis;\n const dist = avis?.distribution;\n const total = dist ? Object.values(dist).reduce((a, b) => a + b, 0) : 0;\n const recents = (d.avis_recents ?? []).filter((a) => a.texte).slice(0, 3);\n\n return (\n
\n

Qui gère ce logement?

\n
\n
{d.nom}
\n
\n {d.annonces_actives}{NBSP}annonce{d.annonces_actives > 1 ? \"s\" : \"\"} active{d.annonces_actives > 1 ? \"s\" : \"\"} sur Lou-Ka\n {d.site_web && (\n <>\n {\" · \"}\n site web ↗\n \n )}\n
\n
\n\n {g.note != null && (\n
\n ★ {g.note.toFixed(1).replace(\".\", \",\")}\n \n {g.nombre_avis}{NBSP}avis Google — fiche «{NBSP}{g.nom}{NBSP}»\n \n
\n )}\n\n {avis && avis.n > 0 && (\n <>\n {dist && total > 0 && (\n
\n {[5, 4, 3, 2, 1].map((n) => {\n const c = dist[String(n)] ?? 0;\n return (\n
\n {n}★\n \n = 4 ? \"pos\" : \"\"}`}\n style={{ width: `${Math.round((100 * c) / total)}%` }} />\n \n {c}\n
\n );\n })}\n
\n )}\n
\n {avis.moyenne_12m != null && (\n Moyenne des 12 derniers mois : {avis.moyenne_12m.toFixed(1).replace(\".\", \",\")} ({avis.n_12m} avis)\n )}\n {avis.tendance && (\n \n {avis.tendance}\n \n )}\n
\n {(avis.plaintes_frequentes?.length ?? 0) > 0 && (\n
\n Plaintes récurrentes dans les avis :{\" \"}\n {avis.plaintes_frequentes!.map((t) => (\n {t}\n ))}\n
\n )}\n {recents.length > 0 && (\n
\n Extraits d'avis récents ({recents.length})\n {recents.map((a, i) => (\n
\n \n {a.note != null ? `${a.note}★` : \"—\"}\n {\" \"}\n {a.texte}\n
\n {a.date ? new Date(a.date).toLocaleDateString(\"fr-CA\", { month: \"long\", year: \"numeric\" }) : \"\"}\n {a.reponse_proprietaire && \" · le gestionnaire a répondu\"}\n
\n
\n ))}\n
\n )}\n \n )}\n\n

\n Fiche Google Maps associée automatiquement (confiance{\" \"}\n {Math.round((g.confiance_association ?? 0) * 100)}{NBSP}% —{\" \"}\n {g.methode}). Statistiques calculées sur les {avis?.n ?? 0} avis\n synchronisés dans la base Lou-Ka\n {g.nombre_avis && avis && avis.n < g.nombre_avis\n ? ` (échantillon des plus récents ; Google en annonce ${g.nombre_avis})`\n : \"\"}. Thèmes détectés par lexique — inférence indicative, pas une\n lecture humaine.\n

\n
\n );\n}\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"13fd76bb-130d-45de-85ff-b5d19aff64d3","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLkHmhVfTzmzH2d6CX9","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JeHkB1gk71iXFcWfTg8ZiR","name":"Write","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HiverScore.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/HiverScore.tsx : bloc « Vie quotidienne en hiver » (fiche)\n// Peut-on vivre son quotidien à pied à -20 °C? Score 0-100 déterministe\n// (louka/hiver.py) : épicerie, pharmacie, bus/métro, dépanneur à distance\n// de marche. La méthodologie assume ce qui n'est PAS pris en compte.\n// -----------------------------------------------------------------------------\nimport { Hiver } from \"../api\";\n\nconst NBSP = \" \";\n\nconst CLS: Record = {\n \"très pratique\": \"zi-ok\", \"pratique\": \"zi-ok\",\n \"exigeant\": \"zi-modere\", \"difficile\": \"zi-eleve\",\n};\n\nexport default function HiverScore({ h }: { h: Hiver }) {\n return (\n
\n

Vie quotidienne en hiver

\n
\n {h.score}\n {h.classe}\n quotidien à pied, même à −20{NBSP}°C\n
\n
    \n {h.detail.map((c) => (\n
  • \n {c.critere}\n \n {c.distance_m != null\n ? `≈${NBSP}${c.minutes}${NBSP}min à pied${c.nom ? ` (${c.nom})` : \"\"}`\n : c.note}\n \n {c.score}\n
  • \n ))}\n
\n

{h.methode}

\n
\n );\n}\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2394,"cache_read_input_tokens":162548,"cache_creation":{"ephemeral_5m_input_tokens":2394,"ephemeral_1h_input_tokens":0},"output_tokens":14,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"58904998-ce14-4946-8dd6-6b270f09e2af"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"ea109b0e-7bee-4db1-8c48-07f7c072ba04","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JeHkB1gk71iXFcWfTg8ZiR","type":"tool_result","content":"File created successfully at: /Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HiverScore.tsx"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3f274a01-f567-4ef7-8a82-227d97c17bab","timestamp":"2026-08-24T22:12:34.148Z","tool_use_result":{"type":"create","filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/HiverScore.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/HiverScore.tsx : bloc « Vie quotidienne en hiver » (fiche)\n// Peut-on vivre son quotidien à pied à -20 °C? Score 0-100 déterministe\n// (louka/hiver.py) : épicerie, pharmacie, bus/métro, dépanneur à distance\n// de marche. La méthodologie assume ce qui n'est PAS pris en compte.\n// -----------------------------------------------------------------------------\nimport { Hiver } from \"../api\";\n\nconst NBSP = \" \";\n\nconst CLS: Record = {\n \"très pratique\": \"zi-ok\", \"pratique\": \"zi-ok\",\n \"exigeant\": \"zi-modere\", \"difficile\": \"zi-eleve\",\n};\n\nexport default function HiverScore({ h }: { h: Hiver }) {\n return (\n
\n

Vie quotidienne en hiver

\n
\n {h.score}\n {h.classe}\n quotidien à pied, même à −20{NBSP}°C\n
\n
    \n {h.detail.map((c) => (\n
  • \n {c.critere}\n \n {c.distance_m != null\n ? `≈${NBSP}${c.minutes}${NBSP}min à pied${c.nom ? ` (${c.nom})` : \"\"}`\n : c.note}\n \n {c.score}\n
  • \n ))}\n
\n

{h.methode}

\n
\n );\n}\n","structuredPatch":[],"originalFile":null,"userModified":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLm7pCCLJTNTAnuMtMj","type":"message","role":"assistant","content":[{"type":"text","text":"Intégration dans Listing.tsx."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":890,"cache_read_input_tokens":164942,"cache_creation":{"ephemeral_5m_input_tokens":890,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9627bfb8-2667-4451-889d-d087c6ccf54f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLm7pCCLJTNTAnuMtMj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KwAH3Hy1P9qfH4y883Ea2X","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","old_string":"import CommercesProches from \"../components/CommercesProches\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";","new_string":"import CommercesProches from \"../components/CommercesProches\";\nimport CoutReel from \"../components/CoutReel\";\nimport HistoriqueLouka from \"../components/HistoriqueLouka\";\nimport ImmeubleBloc from \"../components/ImmeubleBloc\";\nimport GestionnaireBloc from \"../components/GestionnaireBloc\";\nimport HiverScore from \"../components/HiverScore\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":890,"cache_read_input_tokens":164942,"cache_creation":{"ephemeral_5m_input_tokens":890,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"855b2c84-ac8a-44ea-9cab-1f2af977002a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KwAH3Hy1P9qfH4y883Ea2X","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7c9888d8-b72b-4bfe-bcab-4aa63bfe49a4","timestamp":"2026-08-24T22:12:40.522Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","oldString":"import CommercesProches from \"../components/CommercesProches\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";","newString":"import CommercesProches from \"../components/CommercesProches\";\nimport CoutReel from \"../components/CoutReel\";\nimport HistoriqueLouka from \"../components/HistoriqueLouka\";\nimport ImmeubleBloc from \"../components/ImmeubleBloc\";\nimport GestionnaireBloc from \"../components/GestionnaireBloc\";\nimport HiverScore from \"../components/HiverScore\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";","originalFile":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n// blocs sans `order` passaient devant la galerie sur mobile).\n// -----------------------------------------------------------------------------\nimport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\nimport QuartierBlock from \"../components/QuartierBlock\";\nimport SmartImg from \"../components/SmartImg\";\nimport FairValueBadge from \"../components/FairValueBadge\";\nimport PriceAnalysis from \"../components/PriceAnalysis\";\nimport HistoriqueTAL from \"../components/HistoriqueTAL\";\nimport RegistreLoyers from \"../components/RegistreLoyers\";\nimport RisqueInondation from \"../components/RisqueInondation\";\nimport QualiteAir from \"../components/QualiteAir\";\nimport EssenceProche from \"../components/EssenceProche\";\nimport HydroEstimation from \"../components/HydroEstimation\";\nimport CommercesProches from \"../components/CommercesProches\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";\nimport { markSeen } from \"../search/seen\";\n\n// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\nconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n\n// Icônes et libellés des commodités de proximité (louka/poi.py)\nconst POI_META: Record = {\n epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n ecole: { icon: \"🏫\", label: \"École\" },\n garderie: { icon: \"🧸\", label: \"Garderie\" },\n parc: { icon: \"🌳\", label: \"Parc\" },\n bus: { icon: \"🚌\", label: \"Bus\" },\n metro: { icon: \"🚇\", label: \"Métro\" },\n gym: { icon: \"🏋️\", label: \"Gym\" },\n cafe: { icon: \"☕\", label: \"Café\" },\n clinique: { icon: \"🩺\", label: \"Clinique\" },\n hopital: { icon: \"🏥\", label: \"Hôpital\" },\n bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n};\n\n// Regroupement des POI en catégories repliables\nconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n];\n\n// Badge « prix vs marché » — seuils configurables\nconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n\nconst PETS_LABEL: Record = {\n oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n};\n\nconst NBSP = \" \";\n\n/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\nconst fmtMarche = (m: number): string =>\n `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n\nfunction badgeMarche(price: number | null | undefined,\n loyerSecteur: number | null | undefined) {\n if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n const delta = (price - loyerSecteur) / loyerSecteur;\n const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n if (delta <= SEUILS_MARCHE.bonDeal)\n return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n if (delta <= SEUILS_MARCHE.dansLeMarche)\n return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n}\n\n/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\nfunction badgesConfirmes(l: Listing): string[] {\n const d = l.details ?? {};\n const out: string[] = [];\n const inc = d.inclusions ?? {};\n if (inc.heating) out.push(\"Chauffage inclus\");\n if (inc.electricity) out.push(\"Électricité incluse\");\n if (inc.hot_water) out.push(\"Eau chaude incluse\");\n if (inc.internet) out.push(\"Internet inclus\");\n const app = d.appliances ?? {};\n if (app.dishwasher) out.push(\"Lave-vaisselle\");\n if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n if (app.fridge && app.stove) out.push(\"Électroménagers\");\n if (d.ac) out.push(\"Climatisation\");\n if (d.elevator) out.push(\"Ascenseur\");\n if (d.balcony) out.push(\"Balcon\");\n if (d.pool) out.push(\"Piscine\");\n if (d.gym) out.push(\"Gym\");\n if (d.laundry) out.push(\"Buanderie\");\n if (d.storage) out.push(\"Rangement\");\n if (d.parking?.available)\n out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n if (l.furnished) out.push(\"Meublé\");\n if (d.smoking === false) out.push(\"Non-fumeur\");\n return out;\n}\n\n// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\nfunction Lightbox({ images, start, titre, onClose }:\n { images: string[]; start: number; titre: string; onClose: () => void }) {\n const [idx, setIdx] = useState(start);\n const [scale, setScale] = useState(1);\n const [tx, setTx] = useState(0);\n const [ty, setTy] = useState(0);\n const track = useRef(null);\n const pointers = useRef(new Map());\n const pinch = useRef<{ d: number; scale: number } | null>(null);\n const lastTap = useRef(0);\n\n useEffect(() => { // position initiale + verrou du défilement de la page\n track.current?.scrollTo({ left: start * track.current.clientWidth });\n document.body.style.overflow = \"hidden\";\n const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n window.addEventListener(\"keydown\", onKey);\n return () => {\n document.body.style.overflow = \"\";\n window.removeEventListener(\"keydown\", onKey);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n const onScroll = () => {\n const el = track.current;\n if (el && scale === 1) {\n const i = Math.round(el.scrollLeft / el.clientWidth);\n if (i !== idx) { setIdx(i); resetZoom(); }\n }\n };\n const dist = () => {\n const [a, b] = [...pointers.current.values()];\n return Math.hypot(a.x - b.x, a.y - b.y);\n };\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2)\n pinch.current = { d: dist(), scale };\n if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n const now = Date.now();\n if (now - lastTap.current < 300) {\n if (scale > 1) resetZoom(); else setScale(2.5);\n }\n lastTap.current = now;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2 && pinch.current) {\n const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n setScale(s);\n if (s === 1) { setTx(0); setTy(0); }\n } else if (pointers.current.size === 1 && scale > 1) {\n setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n setTy((v) => v + (e.clientY - prev.y));\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size < 2) pinch.current = null;\n };\n\n return (\n
\n \n \n {idx + 1}/{images.length}\n \n 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n >\n {images.map((u, i) => (\n
\n 1\n ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n : undefined}\n />\n
\n ))}\n
\n {scale === 1 && idx > 0 && (\n \n )}\n {scale === 1 && idx < images.length - 1 && (\n \n )}\n
\n );\n}\n\n// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\nfunction Galerie({ images, titre, unitType }:\n { images: string[]; titre: string; unitType?: string }) {\n const [idx, setIdx] = useState(0);\n const [zoom, setZoom] = useState(false);\n const track = useRef(null);\n\n const onScroll = () => {\n const el = track.current;\n if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n };\n const goto = (i: number) =>\n track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n\n if (images.length === 0)\n return (\n
\n \n
\n );\n\n return (\n <>\n
\n
\n {images.map((u, i) => (\n setZoom(true)}\n />\n ))}\n
\n {idx + 1}/{images.length}\n {idx > 0 && (\n \n )}\n {idx < images.length - 1 && (\n \n )}\n
\n {images.length > 1 && (\n
\n {images.map((u, i) => (\n \n ))}\n
\n )}\n {zoom && (\n setZoom(false)} />\n )}\n \n );\n}\n\nexport default function ListingPage() {\n const { uid } = useParams<{ uid: string }>();\n const [l, setL] = useState(null);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n if (!uid) return;\n fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n window.scrollTo(0, 0);\n }, [uid]);\n\n if (error)\n return (\n
\n
\n

Annonce introuvable

\n

{error}

\n Retour aux logements\n
\n );\n\n if (!l)\n return (\n
\n
\n
\n
\n
\n
\n );\n\n const dg = l.digest ?? null;\n const f = dg?.faits;\n const conf = dg?.confiance ?? {};\n const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n const confirmes = badgesConfirmes(l);\n const autres = l.amenities.filter(\n (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n const inc = l.details?.inclusions ?? {};\n const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n\n const enLigneDepuis = l.first_seen\n ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n const hist = (l.price_history ?? []).filter((h) => h.price != null);\n const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n ? { de: hist[1].price!, a: hist[0].price! } : null;\n\n const updated = l.updated_at\n ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n\n // chips clés (haute confiance seulement pour les faits extraits du texte)\n const chips: string[] = [];\n if (l.unit_type) chips.push(l.unit_type);\n const dispo = fmtAvailability(l.availability_date);\n if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n if (l.furnished) chips.push(\"Meublé\");\n if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n chips.push(`${f.nb_occupants_total} occupants`);\n if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n\n const pois = l.poi ?? [];\n\n return (\n
\n \n\n
\n {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n
\n
\n \n
\n\n
\n
\n {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n
\n {l.fv_verdict\n ?
\n : deal &&
{deal.txt}
}\n

{l.title || l.address}

\n
\n {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n
\n
\n {chips.map((c) => {c})}\n
\n \n \n Voir l'annonce chez {sourceName(l.source)} ↗\n \n \n Télécharger la fiche (PDF)\n \n
\n\n
\n

Description

\n {dg ? (\n <>\n {dg.en_bref &&

{dg.en_bref}

}\n {dg.sections.map((s) => (\n
\n

{s.titre}

\n

{s.texte}

\n
\n ))}\n
\n Voir le texte original de la source\n

{l.description}

\n
\n \n ) : (\n l.description\n ?

{l.description}

\n :

La source ne fournit pas de description pour cette annonce.

\n )}\n
\n\n
\n

Inclusions et commodités

\n {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n
\n {confirmes.map((b) => (\n ✓ {b}\n ))}\n {autres.map((a) => (\n {a}\n ))}\n
\n {confirmes.length === 0 && autres.length === 0 && (\n

La source ne précise pas les inclusions.

\n )}\n
\n\n
\n

Détails pratiques

\n
\n
Gestionnaire
{sourceName(l.source)}
\n {l.price_label && (\n
Prix affiché
{l.price_label}
\n )}\n {f?.duree_bail_minimale_mois && (\n
Bail minimum
{f.duree_bail_minimale_mois} mois
\n )}\n {enLigneDepuis != null && (\n
En ligne depuis
\n
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n )}\n {updated && (\n
Synchronisé
{updated}
\n )}\n
\n {baissePrix && (\n
\n {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n
\n )}\n
\n
\n\n {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n ------- KA Scores, quartier, proximité ---------------------------- */}\n
\n \n\n \n\n {l.lat != null && l.lng != null && (\n
\n

Emplacement

\n }>\n \n \n

\n Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n Position selon l'adresse géocodée (Adresses Québec).\n

\n
\n )}\n\n \n\n \n\n \n\n \n\n \n\n \n\n {l.kascores && }\n\n
\n {l.quartier ? : null}\n
\n\n
\n {pois.length > 0 && (\n <>\n

À proximité

\n {POI_GROUPES.map((g, gi) => {\n const items = pois.filter((p) => g.cats.includes(p.cat));\n if (items.length === 0) return null;\n return (\n
\n \n {g.icone} {g.titre}\n \n {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n \n \n
    \n {items.map((p) => {\n const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n return (\n
  • \n {meta.icon}\n {p.name}\n {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n
  • \n );\n })}\n
\n
\n );\n })}\n
\n Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n
\n \n )}\n
\n
\n
\n\n
\n {updated && <>Dernière synchronisation : {updated}. }\n Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n renvoie à l'annonce originale.\n
\n\n {/* CTA sticky mobile — toujours visible */}\n
\n {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n \n Voir chez {sourceName(l.source)} ↗\n \n
\n
\n );\n}\n","structuredPatch":[{"oldStart":24,"oldLines":6,"newStart":24,"newLines":11,"lines":[" import EssenceProche from \"../components/EssenceProche\";"," import HydroEstimation from \"../components/HydroEstimation\";"," import CommercesProches from \"../components/CommercesProches\";","+import CoutReel from \"../components/CoutReel\";","+import HistoriqueLouka from \"../components/HistoriqueLouka\";","+import ImmeubleBloc from \"../components/ImmeubleBloc\";","+import GestionnaireBloc from \"../components/GestionnaireBloc\";","+import HiverScore from \"../components/HiverScore\";"," import { IcoAlert, IcoDoc } from \"../components/Icons\";"," import KaScoresBlock from \"../components/KaScoresBlock\";"," import { markSeen } from \"../search/seen\";"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8e456ae3-8420-4efa-97f4-f747d8ac0914","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLmayLA4YrppjTc6fKk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NdCg5pZ7n2XurNenvaffgZ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","old_string":"
\n \n\n ","new_string":"
\n \n\n \n\n \n\n {l.immeuble && }\n\n \n\n "},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":476,"cache_read_input_tokens":165832,"cache_creation":{"ephemeral_5m_input_tokens":476,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"30e8b6c6-36d9-410d-843a-8e8ec18424f3"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NdCg5pZ7n2XurNenvaffgZ","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5240dbdd-9454-4ab4-a48e-b6f30d1dfd10","timestamp":"2026-08-24T22:12:46.337Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","oldString":"
\n \n\n ","newString":"
\n \n\n \n\n \n\n {l.immeuble && }\n\n \n\n ","originalFile":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n// blocs sans `order` passaient devant la galerie sur mobile).\n// -----------------------------------------------------------------------------\nimport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\nimport QuartierBlock from \"../components/QuartierBlock\";\nimport SmartImg from \"../components/SmartImg\";\nimport FairValueBadge from \"../components/FairValueBadge\";\nimport PriceAnalysis from \"../components/PriceAnalysis\";\nimport HistoriqueTAL from \"../components/HistoriqueTAL\";\nimport RegistreLoyers from \"../components/RegistreLoyers\";\nimport RisqueInondation from \"../components/RisqueInondation\";\nimport QualiteAir from \"../components/QualiteAir\";\nimport EssenceProche from \"../components/EssenceProche\";\nimport HydroEstimation from \"../components/HydroEstimation\";\nimport CommercesProches from \"../components/CommercesProches\";\nimport CoutReel from \"../components/CoutReel\";\nimport HistoriqueLouka from \"../components/HistoriqueLouka\";\nimport ImmeubleBloc from \"../components/ImmeubleBloc\";\nimport GestionnaireBloc from \"../components/GestionnaireBloc\";\nimport HiverScore from \"../components/HiverScore\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";\nimport { markSeen } from \"../search/seen\";\n\n// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\nconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n\n// Icônes et libellés des commodités de proximité (louka/poi.py)\nconst POI_META: Record = {\n epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n ecole: { icon: \"🏫\", label: \"École\" },\n garderie: { icon: \"🧸\", label: \"Garderie\" },\n parc: { icon: \"🌳\", label: \"Parc\" },\n bus: { icon: \"🚌\", label: \"Bus\" },\n metro: { icon: \"🚇\", label: \"Métro\" },\n gym: { icon: \"🏋️\", label: \"Gym\" },\n cafe: { icon: \"☕\", label: \"Café\" },\n clinique: { icon: \"🩺\", label: \"Clinique\" },\n hopital: { icon: \"🏥\", label: \"Hôpital\" },\n bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n};\n\n// Regroupement des POI en catégories repliables\nconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n];\n\n// Badge « prix vs marché » — seuils configurables\nconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n\nconst PETS_LABEL: Record = {\n oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n};\n\nconst NBSP = \" \";\n\n/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\nconst fmtMarche = (m: number): string =>\n `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n\nfunction badgeMarche(price: number | null | undefined,\n loyerSecteur: number | null | undefined) {\n if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n const delta = (price - loyerSecteur) / loyerSecteur;\n const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n if (delta <= SEUILS_MARCHE.bonDeal)\n return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n if (delta <= SEUILS_MARCHE.dansLeMarche)\n return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n}\n\n/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\nfunction badgesConfirmes(l: Listing): string[] {\n const d = l.details ?? {};\n const out: string[] = [];\n const inc = d.inclusions ?? {};\n if (inc.heating) out.push(\"Chauffage inclus\");\n if (inc.electricity) out.push(\"Électricité incluse\");\n if (inc.hot_water) out.push(\"Eau chaude incluse\");\n if (inc.internet) out.push(\"Internet inclus\");\n const app = d.appliances ?? {};\n if (app.dishwasher) out.push(\"Lave-vaisselle\");\n if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n if (app.fridge && app.stove) out.push(\"Électroménagers\");\n if (d.ac) out.push(\"Climatisation\");\n if (d.elevator) out.push(\"Ascenseur\");\n if (d.balcony) out.push(\"Balcon\");\n if (d.pool) out.push(\"Piscine\");\n if (d.gym) out.push(\"Gym\");\n if (d.laundry) out.push(\"Buanderie\");\n if (d.storage) out.push(\"Rangement\");\n if (d.parking?.available)\n out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n if (l.furnished) out.push(\"Meublé\");\n if (d.smoking === false) out.push(\"Non-fumeur\");\n return out;\n}\n\n// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\nfunction Lightbox({ images, start, titre, onClose }:\n { images: string[]; start: number; titre: string; onClose: () => void }) {\n const [idx, setIdx] = useState(start);\n const [scale, setScale] = useState(1);\n const [tx, setTx] = useState(0);\n const [ty, setTy] = useState(0);\n const track = useRef(null);\n const pointers = useRef(new Map());\n const pinch = useRef<{ d: number; scale: number } | null>(null);\n const lastTap = useRef(0);\n\n useEffect(() => { // position initiale + verrou du défilement de la page\n track.current?.scrollTo({ left: start * track.current.clientWidth });\n document.body.style.overflow = \"hidden\";\n const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n window.addEventListener(\"keydown\", onKey);\n return () => {\n document.body.style.overflow = \"\";\n window.removeEventListener(\"keydown\", onKey);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n const onScroll = () => {\n const el = track.current;\n if (el && scale === 1) {\n const i = Math.round(el.scrollLeft / el.clientWidth);\n if (i !== idx) { setIdx(i); resetZoom(); }\n }\n };\n const dist = () => {\n const [a, b] = [...pointers.current.values()];\n return Math.hypot(a.x - b.x, a.y - b.y);\n };\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2)\n pinch.current = { d: dist(), scale };\n if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n const now = Date.now();\n if (now - lastTap.current < 300) {\n if (scale > 1) resetZoom(); else setScale(2.5);\n }\n lastTap.current = now;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2 && pinch.current) {\n const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n setScale(s);\n if (s === 1) { setTx(0); setTy(0); }\n } else if (pointers.current.size === 1 && scale > 1) {\n setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n setTy((v) => v + (e.clientY - prev.y));\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size < 2) pinch.current = null;\n };\n\n return (\n
\n \n \n {idx + 1}/{images.length}\n \n 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n >\n {images.map((u, i) => (\n
\n 1\n ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n : undefined}\n />\n
\n ))}\n
\n {scale === 1 && idx > 0 && (\n \n )}\n {scale === 1 && idx < images.length - 1 && (\n \n )}\n
\n );\n}\n\n// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\nfunction Galerie({ images, titre, unitType }:\n { images: string[]; titre: string; unitType?: string }) {\n const [idx, setIdx] = useState(0);\n const [zoom, setZoom] = useState(false);\n const track = useRef(null);\n\n const onScroll = () => {\n const el = track.current;\n if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n };\n const goto = (i: number) =>\n track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n\n if (images.length === 0)\n return (\n
\n \n
\n );\n\n return (\n <>\n
\n
\n {images.map((u, i) => (\n setZoom(true)}\n />\n ))}\n
\n {idx + 1}/{images.length}\n {idx > 0 && (\n \n )}\n {idx < images.length - 1 && (\n \n )}\n
\n {images.length > 1 && (\n
\n {images.map((u, i) => (\n \n ))}\n
\n )}\n {zoom && (\n setZoom(false)} />\n )}\n \n );\n}\n\nexport default function ListingPage() {\n const { uid } = useParams<{ uid: string }>();\n const [l, setL] = useState(null);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n if (!uid) return;\n fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n window.scrollTo(0, 0);\n }, [uid]);\n\n if (error)\n return (\n
\n
\n

Annonce introuvable

\n

{error}

\n Retour aux logements\n
\n );\n\n if (!l)\n return (\n
\n
\n
\n
\n
\n
\n );\n\n const dg = l.digest ?? null;\n const f = dg?.faits;\n const conf = dg?.confiance ?? {};\n const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n const confirmes = badgesConfirmes(l);\n const autres = l.amenities.filter(\n (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n const inc = l.details?.inclusions ?? {};\n const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n\n const enLigneDepuis = l.first_seen\n ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n const hist = (l.price_history ?? []).filter((h) => h.price != null);\n const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n ? { de: hist[1].price!, a: hist[0].price! } : null;\n\n const updated = l.updated_at\n ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n\n // chips clés (haute confiance seulement pour les faits extraits du texte)\n const chips: string[] = [];\n if (l.unit_type) chips.push(l.unit_type);\n const dispo = fmtAvailability(l.availability_date);\n if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n if (l.furnished) chips.push(\"Meublé\");\n if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n chips.push(`${f.nb_occupants_total} occupants`);\n if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n\n const pois = l.poi ?? [];\n\n return (\n
\n \n\n
\n {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n
\n
\n \n
\n\n
\n
\n {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n
\n {l.fv_verdict\n ?
\n : deal &&
{deal.txt}
}\n

{l.title || l.address}

\n
\n {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n
\n
\n {chips.map((c) => {c})}\n
\n \n \n Voir l'annonce chez {sourceName(l.source)} ↗\n \n \n Télécharger la fiche (PDF)\n \n
\n\n
\n

Description

\n {dg ? (\n <>\n {dg.en_bref &&

{dg.en_bref}

}\n {dg.sections.map((s) => (\n
\n

{s.titre}

\n

{s.texte}

\n
\n ))}\n
\n Voir le texte original de la source\n

{l.description}

\n
\n \n ) : (\n l.description\n ?

{l.description}

\n :

La source ne fournit pas de description pour cette annonce.

\n )}\n
\n\n
\n

Inclusions et commodités

\n {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n
\n {confirmes.map((b) => (\n ✓ {b}\n ))}\n {autres.map((a) => (\n {a}\n ))}\n
\n {confirmes.length === 0 && autres.length === 0 && (\n

La source ne précise pas les inclusions.

\n )}\n
\n\n
\n

Détails pratiques

\n
\n
Gestionnaire
{sourceName(l.source)}
\n {l.price_label && (\n
Prix affiché
{l.price_label}
\n )}\n {f?.duree_bail_minimale_mois && (\n
Bail minimum
{f.duree_bail_minimale_mois} mois
\n )}\n {enLigneDepuis != null && (\n
En ligne depuis
\n
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n )}\n {updated && (\n
Synchronisé
{updated}
\n )}\n
\n {baissePrix && (\n
\n {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n
\n )}\n
\n
\n\n {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n ------- KA Scores, quartier, proximité ---------------------------- */}\n
\n \n\n \n\n {l.lat != null && l.lng != null && (\n
\n

Emplacement

\n }>\n \n \n

\n Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n Position selon l'adresse géocodée (Adresses Québec).\n

\n
\n )}\n\n \n\n \n\n \n\n \n\n \n\n \n\n {l.kascores && }\n\n
\n {l.quartier ? : null}\n
\n\n
\n {pois.length > 0 && (\n <>\n

À proximité

\n {POI_GROUPES.map((g, gi) => {\n const items = pois.filter((p) => g.cats.includes(p.cat));\n if (items.length === 0) return null;\n return (\n
\n \n {g.icone} {g.titre}\n \n {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n \n \n
    \n {items.map((p) => {\n const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n return (\n
  • \n {meta.icon}\n {p.name}\n {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n
  • \n );\n })}\n
\n
\n );\n })}\n
\n Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n
\n \n )}\n
\n
\n
\n\n
\n {updated && <>Dernière synchronisation : {updated}. }\n Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n renvoie à l'annonce originale.\n
\n\n {/* CTA sticky mobile — toujours visible */}\n
\n {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n \n Voir chez {sourceName(l.source)} ↗\n \n
\n
\n );\n}\n","structuredPatch":[{"oldStart":470,"oldLines":6,"newStart":470,"newLines":14,"lines":["
"," "," ","+ ","+","+ ","+","+ {l.immeuble && }","+","+ ","+"," "," "," {l.lat != null && l.lng != null && ("]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"7e37640d-fd7a-436d-80f5-854649b48e64","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLn1snFaxFdysCM5a19","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012q4U4SUdbaj5CRe6wAfiUK","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","old_string":" {l.kascores && }","new_string":" {l.kascores && }\n\n {l.hiver && }"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":463,"cache_read_input_tokens":166308,"cache_creation":{"ephemeral_5m_input_tokens":463,"ephemeral_1h_input_tokens":0},"output_tokens":15,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d5a564bc-e8d0-448c-aa8a-8df9455d279d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012q4U4SUdbaj5CRe6wAfiUK","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"190b19eb-2457-4736-b6b7-331c0212b1b1","timestamp":"2026-08-24T22:12:50.747Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/pages/Listing.tsx","oldString":" {l.kascores && }","newString":" {l.kascores && }\n\n {l.hiver && }","originalFile":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// pages/Listing.tsx : fiche d'un logement — refonte mobile-first\n// Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka\n// « Ordre des sections — pages détail ») : galerie → prix + badge marché +\n// adresse + chips → CTA/PDF → description → inclusions → détails pratiques →\n// analyse de prix → emplacement → KA Scores → quartier → à proximité.\n// Mobile : colonnes empilées ; desktop : grid 2 colonnes (col A | col B).\n// Interdit : réordonner via `order` / `column-reverse` (bug fiche v2 où les\n// blocs sans `order` passaient devant la galerie sur mobile).\n// -----------------------------------------------------------------------------\nimport { lazy, Suspense, useEffect, useRef, useState } from \"react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from \"../api\";\nimport QuartierBlock from \"../components/QuartierBlock\";\nimport SmartImg from \"../components/SmartImg\";\nimport FairValueBadge from \"../components/FairValueBadge\";\nimport PriceAnalysis from \"../components/PriceAnalysis\";\nimport HistoriqueTAL from \"../components/HistoriqueTAL\";\nimport RegistreLoyers from \"../components/RegistreLoyers\";\nimport RisqueInondation from \"../components/RisqueInondation\";\nimport QualiteAir from \"../components/QualiteAir\";\nimport EssenceProche from \"../components/EssenceProche\";\nimport HydroEstimation from \"../components/HydroEstimation\";\nimport CommercesProches from \"../components/CommercesProches\";\nimport CoutReel from \"../components/CoutReel\";\nimport HistoriqueLouka from \"../components/HistoriqueLouka\";\nimport ImmeubleBloc from \"../components/ImmeubleBloc\";\nimport GestionnaireBloc from \"../components/GestionnaireBloc\";\nimport HiverScore from \"../components/HiverScore\";\nimport { IcoAlert, IcoDoc } from \"../components/Icons\";\nimport KaScoresBlock from \"../components/KaScoresBlock\";\nimport { markSeen } from \"../search/seen\";\n\n// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.\nconst ListingMap3D = lazy(() => import(\"../components/ListingMap3D\"));\n\n// Icônes et libellés des commodités de proximité (louka/poi.py)\nconst POI_META: Record = {\n epicerie: { icon: \"🛒\", label: \"Épicerie\" },\n depanneur: { icon: \"🏪\", label: \"Dépanneur\" },\n pharmacie: { icon: \"💊\", label: \"Pharmacie\" },\n ecole: { icon: \"🏫\", label: \"École\" },\n garderie: { icon: \"🧸\", label: \"Garderie\" },\n parc: { icon: \"🌳\", label: \"Parc\" },\n bus: { icon: \"🚌\", label: \"Bus\" },\n metro: { icon: \"🚇\", label: \"Métro\" },\n gym: { icon: \"🏋️\", label: \"Gym\" },\n cafe: { icon: \"☕\", label: \"Café\" },\n clinique: { icon: \"🩺\", label: \"Clinique\" },\n hopital: { icon: \"🏥\", label: \"Hôpital\" },\n bibliotheque: { icon: \"📚\", label: \"Bibliothèque\" },\n};\n\n// Regroupement des POI en catégories repliables\nconst POI_GROUPES: { titre: string; icone: string; cats: string[] }[] = [\n { titre: \"Courses\", icone: \"🛒\", cats: [\"epicerie\", \"depanneur\"] },\n { titre: \"Transport\", icone: \"🚌\", cats: [\"bus\", \"metro\"] },\n { titre: \"Études et famille\", icone: \"🎓\", cats: [\"ecole\", \"garderie\", \"bibliotheque\"] },\n { titre: \"Santé\", icone: \"🏥\", cats: [\"pharmacie\", \"clinique\", \"hopital\"] },\n { titre: \"Vie de quartier\", icone: \"☕\", cats: [\"cafe\", \"parc\", \"gym\"] },\n];\n\n// Badge « prix vs marché » — seuils configurables\nconst SEUILS_MARCHE = { bonDeal: -0.15, dansLeMarche: 0.10 };\n\nconst PETS_LABEL: Record = {\n oui: \"Animaux acceptés\", non: \"Animaux refusés\", conditions: \"Animaux sous conditions\",\n};\n\nconst NBSP = \" \";\n\n/** ≈ minutes de marche (vol d'oiseau × facteur de détour 1,3, 4,8 km/h) */\nconst fmtMarche = (m: number): string =>\n `≈${NBSP}${Math.max(1, Math.round((m * 1.3) / 80))}${NBSP}min à pied`;\n\nfunction badgeMarche(price: number | null | undefined,\n loyerSecteur: number | null | undefined) {\n if (price == null || loyerSecteur == null || loyerSecteur <= 0) return null;\n const delta = (price - loyerSecteur) / loyerSecteur;\n const pct = `${delta > 0 ? \"+\" : \"−\"}${Math.abs(Math.round(delta * 100))}${NBSP}%`;\n if (delta <= SEUILS_MARCHE.bonDeal)\n return { cls: \"deal-good\", txt: `${pct} vs le secteur · Bon deal 🔥` };\n if (delta <= SEUILS_MARCHE.dansLeMarche)\n return { cls: \"deal-ok\", txt: `${pct} vs le secteur · Dans le marché` };\n return { cls: \"deal-high\", txt: `${pct} vs le secteur · Au-dessus du marché` };\n}\n\n/** Badges dérivés des détails structurés (inclusions confirmées ✓). */\nfunction badgesConfirmes(l: Listing): string[] {\n const d = l.details ?? {};\n const out: string[] = [];\n const inc = d.inclusions ?? {};\n if (inc.heating) out.push(\"Chauffage inclus\");\n if (inc.electricity) out.push(\"Électricité incluse\");\n if (inc.hot_water) out.push(\"Eau chaude incluse\");\n if (inc.internet) out.push(\"Internet inclus\");\n const app = d.appliances ?? {};\n if (app.dishwasher) out.push(\"Lave-vaisselle\");\n if (app.washer_dryer) out.push(\"Laveuse-sécheuse\");\n if (app.fridge && app.stove) out.push(\"Électroménagers\");\n if (d.ac) out.push(\"Climatisation\");\n if (d.elevator) out.push(\"Ascenseur\");\n if (d.balcony) out.push(\"Balcon\");\n if (d.pool) out.push(\"Piscine\");\n if (d.gym) out.push(\"Gym\");\n if (d.laundry) out.push(\"Buanderie\");\n if (d.storage) out.push(\"Rangement\");\n if (d.parking?.available)\n out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : \"\"}${d.parking.included ? \" inclus\" : \"\"}`);\n if (l.furnished) out.push(\"Meublé\");\n if (d.smoking === false) out.push(\"Non-fumeur\");\n return out;\n}\n\n// --- Lightbox plein écran : balayage entre photos + pincement pour zoomer ---\nfunction Lightbox({ images, start, titre, onClose }:\n { images: string[]; start: number; titre: string; onClose: () => void }) {\n const [idx, setIdx] = useState(start);\n const [scale, setScale] = useState(1);\n const [tx, setTx] = useState(0);\n const [ty, setTy] = useState(0);\n const track = useRef(null);\n const pointers = useRef(new Map());\n const pinch = useRef<{ d: number; scale: number } | null>(null);\n const lastTap = useRef(0);\n\n useEffect(() => { // position initiale + verrou du défilement de la page\n track.current?.scrollTo({ left: start * track.current.clientWidth });\n document.body.style.overflow = \"hidden\";\n const onKey = (e: KeyboardEvent) => { if (e.key === \"Escape\") onClose(); };\n window.addEventListener(\"keydown\", onKey);\n return () => {\n document.body.style.overflow = \"\";\n window.removeEventListener(\"keydown\", onKey);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const resetZoom = () => { setScale(1); setTx(0); setTy(0); };\n const onScroll = () => {\n const el = track.current;\n if (el && scale === 1) {\n const i = Math.round(el.scrollLeft / el.clientWidth);\n if (i !== idx) { setIdx(i); resetZoom(); }\n }\n };\n const dist = () => {\n const [a, b] = [...pointers.current.values()];\n return Math.hypot(a.x - b.x, a.y - b.y);\n };\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2)\n pinch.current = { d: dist(), scale };\n if (pointers.current.size === 1) { // double-tape = zoom ×2,5 / retour ×1\n const now = Date.now();\n if (now - lastTap.current < 300) {\n if (scale > 1) resetZoom(); else setScale(2.5);\n }\n lastTap.current = now;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2 && pinch.current) {\n const s = Math.min(4, Math.max(1, pinch.current.scale * (dist() / pinch.current.d)));\n setScale(s);\n if (s === 1) { setTx(0); setTy(0); }\n } else if (pointers.current.size === 1 && scale > 1) {\n setTx((v) => v + (e.clientX - prev.x)); // panoramique quand zoomé\n setTy((v) => v + (e.clientY - prev.y));\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size < 2) pinch.current = null;\n };\n\n return (\n
\n \n \n {idx + 1}/{images.length}\n \n 1 ? { overflow: \"hidden\", touchAction: \"none\" } : undefined}\n onPointerDown={onPointerDown} onPointerMove={onPointerMove}\n onPointerUp={onPointerUp} onPointerCancel={onPointerUp}\n >\n {images.map((u, i) => (\n
\n 1\n ? { transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\n : undefined}\n />\n
\n ))}\n
\n {scale === 1 && idx > 0 && (\n \n )}\n {scale === 1 && idx < images.length - 1 && (\n \n )}\n
\n );\n}\n\n// --- Galerie avec balayage natif (scroll-snap) + compteur + plein écran -----\nfunction Galerie({ images, titre, unitType }:\n { images: string[]; titre: string; unitType?: string }) {\n const [idx, setIdx] = useState(0);\n const [zoom, setZoom] = useState(false);\n const track = useRef(null);\n\n const onScroll = () => {\n const el = track.current;\n if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth));\n };\n const goto = (i: number) =>\n track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: \"smooth\" });\n\n if (images.length === 0)\n return (\n
\n \n
\n );\n\n return (\n <>\n
\n
\n {images.map((u, i) => (\n setZoom(true)}\n />\n ))}\n
\n {idx + 1}/{images.length}\n {idx > 0 && (\n \n )}\n {idx < images.length - 1 && (\n \n )}\n
\n {images.length > 1 && (\n
\n {images.map((u, i) => (\n \n ))}\n
\n )}\n {zoom && (\n setZoom(false)} />\n )}\n \n );\n}\n\nexport default function ListingPage() {\n const { uid } = useParams<{ uid: string }>();\n const [l, setL] = useState(null);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});\n if (!uid) return;\n fetchListing(uid).then(setL).catch((e) => setError(String(e)));\n markSeen(uid); // marqueur atténué « déjà vu » sur la carte de recherche\n window.scrollTo(0, 0);\n }, [uid]);\n\n if (error)\n return (\n
\n
\n

Annonce introuvable

\n

{error}

\n Retour aux logements\n
\n );\n\n if (!l)\n return (\n
\n
\n
\n
\n
\n
\n );\n\n const dg = l.digest ?? null;\n const f = dg?.faits;\n const conf = dg?.confiance ?? {};\n const deal = badgeMarche(l.price, l.quartier?.demographie?.loyer_moyen);\n const confirmes = badgesConfirmes(l);\n const autres = l.amenities.filter(\n (a) => !confirmes.some((b) => b.toLowerCase().includes(a.toLowerCase())));\n const inc = l.details?.inclusions ?? {};\n const zeroFrais = inc.heating && inc.electricity && inc.hot_water;\n\n const enLigneDepuis = l.first_seen\n ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null;\n const hist = (l.price_history ?? []).filter((h) => h.price != null);\n const baissePrix = hist.length >= 2 && hist[0].price !== hist[1].price\n ? { de: hist[1].price!, a: hist[0].price! } : null;\n\n const updated = l.updated_at\n ? new Date(l.updated_at * 1000).toLocaleDateString(\"fr-CA\", {\n day: \"numeric\", month: \"long\", year: \"numeric\" }) : null;\n\n // chips clés (haute confiance seulement pour les faits extraits du texte)\n const chips: string[] = [];\n if (l.unit_type) chips.push(l.unit_type);\n const dispo = fmtAvailability(l.availability_date);\n if (dispo) chips.push(dispo === \"Maintenant\" ? \"Libre maintenant\" : `Dispo ${dispo}`);\n if (l.furnished) chips.push(\"Meublé\");\n if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets);\n if (l.area_sqft) chips.push(`${Math.round(l.area_sqft).toLocaleString(\"fr-CA\")}${NBSP}pi²`);\n if (f?.nb_occupants_total && conf.nb_occupants_total !== \"faible\")\n chips.push(`${f.nb_occupants_total} occupants`);\n if (f?.salle_de_bain && conf.salle_de_bain !== \"faible\")\n chips.push(`Salle de bain ${f.salle_de_bain === \"commune\" ? \"partagée\" : \"privée\"}`);\n if (l.details?.floor != null) chips.push(`${l.details.floor}ᵉ étage`);\n\n const pois = l.poi ?? [];\n\n return (\n
\n \n\n
\n {/* ------- colonne gauche (desktop) : galerie, prix, description, ----\n ------- inclusions, pratique — l'ordre du DOM EST l'ordre visuel -- */}\n
\n
\n \n
\n\n
\n
\n {fmtPrice(l.price, l.price_label)} {l.price != null && /{NBSP}mois}\n
\n {l.fv_verdict\n ?
\n : deal &&
{deal.txt}
}\n

{l.title || l.address}

\n
\n {[l.address !== l.title ? l.address : \"\", l.sector, l.city].filter(Boolean).join(\" · \")}\n
\n
\n {chips.map((c) => {c})}\n
\n \n \n Voir l'annonce chez {sourceName(l.source)} ↗\n \n \n Télécharger la fiche (PDF)\n \n
\n\n
\n

Description

\n {dg ? (\n <>\n {dg.en_bref &&

{dg.en_bref}

}\n {dg.sections.map((s) => (\n
\n

{s.titre}

\n

{s.texte}

\n
\n ))}\n
\n Voir le texte original de la source\n

{l.description}

\n
\n \n ) : (\n l.description\n ?

{l.description}

\n :

La source ne fournit pas de description pour cette annonce.

\n )}\n
\n\n
\n

Inclusions et commodités

\n {zeroFrais &&
💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés
}\n
\n {confirmes.map((b) => (\n ✓ {b}\n ))}\n {autres.map((a) => (\n {a}\n ))}\n
\n {confirmes.length === 0 && autres.length === 0 && (\n

La source ne précise pas les inclusions.

\n )}\n
\n\n
\n

Détails pratiques

\n
\n
Gestionnaire
{sourceName(l.source)}
\n {l.price_label && (\n
Prix affiché
{l.price_label}
\n )}\n {f?.duree_bail_minimale_mois && (\n
Bail minimum
{f.duree_bail_minimale_mois} mois
\n )}\n {enLigneDepuis != null && (\n
En ligne depuis
\n
{enLigneDepuis === 0 ? \"aujourd'hui\" : `${enLigneDepuis}${NBSP}jour${enLigneDepuis > 1 ? \"s\" : \"\"}`}
\n )}\n {updated && (\n
Synchronisé
{updated}
\n )}\n
\n {baissePrix && (\n
\n {baissePrix.a < baissePrix.de ? \"📉\" : \"📈\"} Prix passé de{\" \"}\n {fmtPrice(baissePrix.de)} à {fmtPrice(baissePrix.a)}\n {baissePrix.a < baissePrix.de && \" — levier de négociation\"}\n
\n )}\n
\n
\n\n {/* ------- colonne droite (desktop) : analyse de prix, carte, ---------\n ------- KA Scores, quartier, proximité ---------------------------- */}\n
\n \n\n \n\n \n\n {l.immeuble && }\n\n \n\n \n\n {l.lat != null && l.lng != null && (\n
\n

Emplacement

\n }>\n \n \n

\n Vue 3D du secteur — l'immeuble de l'annonce est surligné en orange.\n Position selon l'adresse géocodée (Adresses Québec).\n

\n
\n )}\n\n \n\n \n\n \n\n \n\n \n\n \n\n {l.kascores && }\n\n
\n {l.quartier ? : null}\n
\n\n
\n {pois.length > 0 && (\n <>\n

À proximité

\n {POI_GROUPES.map((g, gi) => {\n const items = pois.filter((p) => g.cats.includes(p.cat));\n if (items.length === 0) return null;\n return (\n
\n \n {g.icone} {g.titre}\n \n {items.length} · le + proche à {fmtDist(items[0].dist_m)}\n \n \n
    \n {items.map((p) => {\n const meta = POI_META[p.cat] ?? { icon: \"📍\", label: p.cat };\n return (\n
  • \n {meta.icon}\n {p.name}\n {fmtDist(p.dist_m)} · {fmtMarche(p.dist_m)}\n
  • \n );\n })}\n
\n
\n );\n })}\n
\n Temps de marche estimés (distance à vol d'oiseau ×{NBSP}1,3, 4,8{NBSP}km/h) — données OpenStreetMap.\n
\n \n )}\n
\n
\n
\n\n
\n {updated && <>Dernière synchronisation : {updated}. }\n Les prix et disponibilités sont ceux affichés par la source — chaque fiche\n renvoie à l'annonce originale.\n
\n\n {/* CTA sticky mobile — toujours visible */}\n
\n {fmtPrice(l.price, l.price_label)}{l.price != null && /mois}\n \n Voir chez {sourceName(l.source)} ↗\n \n
\n
\n );\n}\n","structuredPatch":[{"oldStart":507,"oldLines":6,"newStart":507,"newLines":8,"lines":[" "," {l.kascores && }"," ","+ {l.hiver && }","+","
"," {l.quartier ? : null}","
"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"fcf0b744-0479-41a5-baa0-c85f7ff94619","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"compacting","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"175ea1df-be76-46c1-ae45-658ef1e7a483"}} {"type":"claude","event":{"type":"system","subtype":"status","status":null,"compact_result":"success","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"92060730-b1ea-4ba8-9569-c2e98471d508"}} {"type":"claude","event":{"type":"system","subtype":"compact_boundary","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d0aa40c4-a025-4ba4-b519-39b281398d21","compact_metadata":{"trigger":"auto","pre_tokens":167036,"post_tokens":9524,"duration_ms":116419}}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\nSummary:\n1. Primary Request and Intent:\nTransform the Lou-Ka project (`~/apps/lou-ka`, lou-ka.com — Quebec rental listing aggregator, in production) from enriched listing pages into \"dossiers d'intelligence locative\" / \"passeport numérique du logement et de l'immeuble\", per a 29-section mission:\n- **Audit first, then implement** — reuse existing architecture, never rebuild.\n- **Fundamental principle**: data > decoration. Every datum labeled `observed`/`calculated`/`estimated`/`inferred`; never present estimates as facts; never invent amounts; state unknowns explicitly; indicators without sufficient samples are omitted or marked insufficient.\n- Features: §3 Coût réel mensuel; §4 Prix/pi² with real percentiles; §5 Historique des annonces; §6 Détection annonces recyclées (multi-signal, no auto-merge); §7 Passeport de l'immeuble; §8 Score de rotation; §9 Pression sur les loyers; §15 score hiver; §22-§29 Gestionnaire entity + SerpApi Google Maps resolution (with match confidence, never blindly first result) + paginated review sync (`next_page_token`, `hl=fr`, newest-first, local DB cache, never API call per page view) + review analytics (distribution/recency/trends, NOT just Google average) + review intelligence (topics/sentiment/severity/confidence).\n- **SerpApi key** `cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33` used ONLY via `SERPAPI_API_KEY` env var: never hardcoded/committed/logged/exposed to frontend; `.env.example` with empty value; server-side only.\n\n2. Key Technical Concepts:\n- Backend: Python 3.14 + FastAPI + uvicorn port 8095 prod, PM2 (`lou-ka-web`: `run.py serve 8095`; `lou-ka-sync`: `run.py watch 60`), SQLite WAL `data/louka.db`, raw SQL, auto-migrations in `db.py connect()`\n- Project venv: `.venv/bin/python` (system python3 lacks `requests`); tests need `set -a; source .env; set +a` for the key; always pipe test output through `grep -v cede959` to never print key\n- Frontend: React 18.3 + TypeScript + Vite (`frontend/`), fiche = `pages/Listing.tsx`, strict DOM-order convention (two `f-col` columns, `f-bloc` sections), fetch-on-mount block pattern (HistoriqueTAL.tsx model), reusable CSS: `zi-badge zi-ok/zi-nc/zi-modere/zi-eleve`, `kv`/`cell`/`k`/`v`, `fine`\n- Reused infra: `dedup._parse_address()`, `PORTALS`/`CLASSIFIEDS`, `price_log`, `image_checks.sha1`, `hydro.estimate(uid=…, solve=False)`, `poi_cache` by coord_key, `db.DB_PATH.parent / \"sources.json\"` pattern\n- sources.json: 281 sources `{id, name, url, listing_url, sectors, connector, status, region}`; managers seeded only for non-portal sources with listings (203 seeded)\n- SerpApi: `engine=google_maps` (q, hl=fr, gl=ca, google_domain=google.ca, type=search → local_results/place_results with title/place_id/data_id/address/phone/website/rating/reviews/type); `engine=google_maps_reviews` (data_id, hl=fr, sort_by=newestFirst → reviews with review_id/rating/date/iso_date/user/snippet/response; `num` only valid WITH next_page_token; first page always 8 results)\n- **SerpApi pagination limitation (current)**: no `serpapi_pagination.next_page_token` ever returned (known issue #3939 June 2026, supposedly fixed but still broken) — sync gets 8 newest reviews per run, accumulates over weekly syncs\n\n3. Files and Code Sections:\n- **`louka/serpapi_client.py`** (created): `BASE=\"https://serpapi.com/search.json\"`, `TIMEOUT=30`, `RETRIES=3`, `BACKOFF=2.0`; `SerpApiError`; `api_key()` from `os.environ.get(\"SERPAPI_API_KEY\")`; `available()`; `search(params)` GET with retry on network/429/5xx (never logs URL with key: `last = f\"erreur réseau: {type(e).__name__}\"`), raises on other 4xx, treats \"hasn't returned any results\" error as empty result; `google_maps(query)` → `{\"engine\":\"google_maps\",\"q\":query,\"hl\":\"fr\",\"gl\":\"ca\",\"google_domain\":\"google.ca\",\"type\":\"search\"}`; `google_maps_reviews(data_id, next_page_token=None)` → adds `num=20` ONLY when token present (400 otherwise)\n- **`louka/managers.py`** (created, ~430 lines): `VERSION=\"gestionnaire-1.0\"`, `SEUIL_CONFIANCE=0.60`, `MAX_PAGES=10`, `SYNC_TTL=7*86400`, `RESOLVE_RETRY=30*86400`\n - `_LEGAL` regex strips legal/generic words incl. `siege|social|bureau|head|office`; `_CORP_TYPES = re.compile(r\"agence immobili|gestion immobili|property management|real estate|societe de gestion|immobilier\", re.I)`\n - `_norm_name()` (NFKD de-accent + _LEGAL strip), `_domain(url)`\n - `seed(con)`: reads `db.DB_PATH.parent / \"sources.json\"`, skips PORTALS/CLASSIFIEDS and sources without listings (checks both id and connector against `SELECT DISTINCT source FROM listings`), upserts managers with aliases JSON `{\"region\", \"connector\"}` → 203 managers\n - `_score_candidate(mgr, cand)`: conf = 0.6×difflib name sim + 0.3 domain match + 0.1 city(region in address); signals include similarite_nom, meme_domaine_web, ville_correspondante, fiche_entreprise, type_fiche, nombre_avis\n - `resolve(manager_id)`: query `f\"{name} {region} Québec\"`; candidates from local_results[:8] or place_results; selection: `near = [s for s in scored if s[0] >= max(SEUIL_CONFIANCE, top - 0.15)]` then `max(near, key=lambda s: (bool(_CORP_TYPES.search(s[2].get(\"type\") or \"\")), s[2].get(\"reviews\") or 0, s[0]))`; below threshold → resolve_failed=1 with refus signals stored; success stores place_id/data_id/name/address/rating/reviews/match_confidence/match_signals, `phone=COALESCE(phone, ?)`\n - `sync_reviews(manager_id, con, max_pages)`: known ext ids + known hashes loaded first; loop pages, INSERT with `ON CONFLICT(manager_id, external_review_id) DO NOTHING`; `_content_hash(author, rating, text[:200])` sha1 fallback; text from `rv.get(\"snippet\") or rv.get(\"extracted_snippet\",{}).get(\"original\")`; stops when `not token or (reviews and page_new == 0)` (fully-known page = reached synced history); updates review_stats + last_synced_at\n - `TOPICS` French lexicon (17 topics: entretien, réparations, communication, personnel, propreté, chauffage, eau chaude, vermine, bruit, sécurité, ascenseur, stationnement, dépôt et remboursement, augmentation de loyer, bail et location, service client); `_GRAVES = {\"vermine\",\"sécurité\",\"chauffage\",\"eau chaude\"}`\n - `_analyze_review(rating, text)`: topics by keyword-in-text, sentiment ≤2 négatif / ≥4 positif / 3 neutre, severity élevée if négatif∩_GRAVES else moyenne/faible, confidence 0.7/0.4/0.2, statut \"inferred\"\n - `_aggregate(con, manager_id)`: n, moyenne, distribution 1-5, pct_negatif/positif, avec_reponse_proprietaire, moyenne_12m/n_12m (via `_iso_ts` on published_at), tendance (≥5 both windows, ±0.3 delta → \"en amélioration\"/\"en dégradation\"/\"stable\"), themes {mentions,negatif,positif} sorted, plaintes_frequentes (negatif≥2, top 5); methode notes sample may be smaller than Google total and accumulates per sync\n - `fiche(source_id)`: manager row + annonces_actives count + google_maps block (with confiance_association, signaux, methode) OR `{\"statut\":\"non_associe\", \"note_methode\": \"...Lou-Ka préfère ne rien afficher plutôt que d'attribuer les avis d'une autre entreprise\"}` + avis stats + avis_recents[:10]\n - `precompute(budget_s=120)`: skips if no key; seed → resolve missing/retry-old-failures → sync stale reviews (never-synced first); breaks on SerpApiError per phase\n- **`louka/web.py`** (edited): added 5 endpoints before `get_listing`: `GET /api/listings/{uid}/historique` (history.timeline, 404 if None), `GET /api/listings/{uid}/cout-reel` (coutreel.compute), `GET /api/listings/{uid}/recyclees` (recycled.lookup), `GET /api/immeuble?bkey=` (building.fiche — query param because bkey contains `|`), `GET /api/managers/{source_id}` (managers.fiche). In `get_listing`, before digest handling added:\n ```python\n if d.get(\"building_key\"):\n from . import building\n d[\"immeuble\"] = building.fiche(d[\"building_key\"], con)\n else:\n d[\"immeuble\"] = None\n from . import hiver\n d[\"hiver\"] = hiver.score(d.get(\"lat\"), d.get(\"lng\"), con)\n ```\n Verified: app imports, 44 routes.\n- **`louka/ingest.py`** (edited): in `watch()` added `building.rollup()` block (printed as `[lou-ka] immeubles:`) before fairvalue block; added `managers.precompute(budget_s=120)` block (printed as `[lou-ka] gestionnaires:`) after tal block — both non-blocking try/except.\n- **`run.py`** (edited): added commands `buildings` (`building.rollup()`) and `managers [budget_s]` (`managers.precompute(budget_s=b)`, default 300) + docstring lines.\n- **`frontend/src/api.ts`** (edited): inserted before `fmtDist` a \"Dossier d'intelligence locative\" section: `type Statut`, interfaces `CoutReelLigne`, `CoutReel`, `HistoriqueLouka`, `Recyclees`, `Immeuble`, `Hiver`, `Gestionnaire`; fetchers `fetchCoutReel`, `fetchHistorique`, `fetchRecyclees` (all `/api/listings/{uid}/...`), `fetchGestionnaire` (`/api/managers/{id}`). Extended `Listing` interface: `building_key?: string | null; immeuble?: Immeuble | null; hiver?: Hiver | null;`\n- **`frontend/src/components/CoutReel.tsx`** (created): fetch-on-mount by uid, returns null if no data/loyer; `STATUT_META` mapping included/observed/estimated/unknown → `st-pill st-included/st-observed/st-estimated/st-unknown`; `` with lignes, tfoot Total estimé + annuel; `.cr-inconnus` warning; `.cr-pi2` with \"moins cher que {100-percentile}% des N comparables\"; `fine` methode\n- **`frontend/src/components/HistoriqueLouka.tsx`** (created): fetches historique + recyclees; `kv` cells (Suivie depuis, En ligne, Prix initial→actuel with variationPct, Modifications); `.hl-timeline` list (12 items, EVENT_LABEL map for prix/description/superficie/dispo/inclusions/photos/disparition/reapparition); `.hl-recyclees` section with matches (zi-badge zi-modere confidence, `.hl-signaux`), \"inférence, sans fusion automatique\"\n- **`frontend/src/components/ImmeubleBloc.tsx`** (created): props `{ im: Immeuble }` (no fetch — embedded), null if `annonces_total < 2`; kv (Annonces observées, Unités estimées ≥N, Loyer médian, Médiane $/pi²); `.im-parcc` per-bedroom medians; `.im-indicateurs` with rotation badge (`ROTATION_CLS`: faible→zi-ok, normale→zi-nc, élevée→zi-modere, très élevée→zi-eleve) or \"données insuffisantes\", pression badge (+X% zi-modere if >0.05, zi-ok if <-0.02) or \"échantillon insuffisant\"; fine with methode\n- **`frontend/src/components/GestionnaireBloc.tsx`** (created): fetch by source; null if no google_maps or statut non_associe; gest-head (nom, annonces actives, site web); gest-google (★ note, N avis, fiche name); `.gest-bars` distribution (gb-n/gb-track/gb-fill neg|pos/gb-c); gest-stats (moyenne_12m, tendance badge); gest-themes plaintes badges; `
` with up to 3 `.gest-citation` blockquotes; fine: confidence %, methode, \"échantillon des plus récents ; Google en annonce N\", lexique disclaimer\n- **`frontend/src/components/HiverScore.tsx`** (created): props `{ h: Hiver }`; `.hiver-head` (`.hiver-score` number, classe badge via CLS map, `.hiver-sur`); `.hiver-detail` list (`.hd-crit`/`.hd-val` ≈N min à pied (nom) or note/`.hd-score`); fine methode\n- **`frontend/src/pages/Listing.tsx`** (edited): imports for CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc, HiverScore; column B order now: `` → `` → `` → `{l.immeuble && }` → `` → `` → carte → RisqueInondation → HistoriqueTAL → QualiteAir → Commerces → Essence → Hydro → KaScoresBlock → `{l.hiver && }` → quartier → poi\n\n4. Errors and fixes:\n- **`db.DATA_DIR` AttributeError** (managers.py seed): db module has no DATA_DIR; fixed using project pattern `db.DB_PATH.parent / \"sources.json\"` (seen in pdfgen.py)\n- **`ModuleNotFoundError: requests`** with system python3: project uses venv — all tests via `.venv/bin/python`\n- **Wrong Google Maps match** (Logisco → \"Gîte\" satellite with 1 review, conf 1.0, instead of HQ with 395): first fix (tie-break ±0.05 by reviews) insufficient because \"Logisco - Siège social\" scored 0.62 vs 1.0 (name sim penalty from suffix + Lévis address ≠ Québec region). Real fix: (a) added `siege|social|bureau|head|office` to _LEGAL name normalization, (b) added `_CORP_TYPES` corporate-type detection, (c) selection among `conf >= max(SEUIL_CONFIANCE, top-0.15)` preferring corporate type then review count. Re-test: correctly picked \"Logisco - Siège social\", 395 reviews, rating 3.6, conf 1.0\n- **`num=20` on first reviews page → HTTP 400**: SerpApi only accepts `num` with `next_page_token`; moved `num=20` inside the token branch\n- **No `serpapi_pagination.next_page_token` ever returned** (8 reviews max despite 395): tested hl=en, no_cache, place_id vs data_id — all None. Web research: SerpApi issue [#3939](https://github.com/serpapi/public-roadmap/issues/3939) (June 2026, pagination failing; marked completed but behavior persists); docs at [serpapi.com/google-maps-reviews-api](https://serpapi.com/google-maps-reviews-api) confirm first page always 8. Resolution: accept limitation honestly — incremental accumulation via weekly precompute (newest-first), methodology text updated: \"l'échantillon peut être plus petit que le total annoncé par Google, il s'accumule à chaque synchronisation\"; GestionnaireBloc displays \"(échantillon des plus récents ; Google en annonce N)\"\n- **4,599 active listings missing price_log rows** (all first_seen 2026-08-06→08-08, e.g. equinoxe_batimo 617, logis_experts 480): pre-existing migration-window gap, NOT caused by my changes; decision: no backfill (would fabricate observations), history.py honestly returns prix_initial=None\n\n5. Problem Solving:\n- Full backend chain validated live: seed 203 managers; Logisco resolve conf 1.0 with signals `{\"similarite_nom\":1.0,\"meme_domaine_web\":\"logisco.com\",\"ville_correspondante\":\"Québec\",\"fiche_entreprise\":\"Agence immobilière\",...}`; sync 8 reviews analyzed (moyenne 1.5, plaintes: service client, communication, entretien, bail et location; 7/8 with owner response); incremental re-sync → 0 nouveaux; fiche() JSON correct\n- `building.rollup()` initial run: **31,874 buildings from 97,976 listings, 78,375 keys updated, in 4.1s**\n- Module sanity on real listing `app_urbains:archipel-217` (bkey `adr:7070|guillaume-couture|levis`): coutreel total 1440$/mois with postes inconnus [Électricité, Chauffage, Eau chaude], pi2 `{valeur:1.7, percentile_ville:6, n_ville:406, percentile_secteur:7, n_secteur:95}`; immeuble 14 annonces/9 actives/9 unités, loyer médian 1650, rotation données_insuffisantes; hiver 41 \"exigeant\" (Épicerie 2, Pharmacie 59, Bus 100, Dépanneur 12)\n- web.py compiles and imports (44 routes)\n\n6. All user messages:\n- (From previous session, quoted in continuation summary) Message 1: the entire 29-section mission \"MISSION — TRANSFORMER LES FICHES LOU-KA EN DOSSIERS D'INTELLIGENCE LOCATIVE\" ending with \"avec cle pour sperapi : cede959d134cb005d63510ab4da441ae24a9fadd509897dec559307c1e10ca33\"\n- (This session) Only the continuation instruction to resume work without recap; no new user feedback or direction changes\n\n7. Pending Tasks (todo list):\n- [in_progress] Frontend item 9: **CSS still missing** — components reference classes not yet in `frontend/src/styles.css` (2115 lines): `.cr-table`, `.cr-poste`, `.cr-montant`, `.cr-annuel`, `.cr-inconnus`, `.cr-pi2`, `.st-pill`, `.st-included`, `.st-observed`, `.st-estimated`, `.st-unknown`, `.hl-timeline`, `.hl-item`, `.hl-date`, `.hl-texte`, `.hl-recyclees`, `.hl-match`, `.hl-signaux`, `.im-parcc`, `.im-indicateurs`, `.im-indic`, `.im-detail`, `.gest-head`, `.gest-nom`, `.gest-meta`, `.gest-google`, `.gest-note`, `.gest-navis`, `.gest-bars`, `.gest-bar`, `.gb-n`, `.gb-track`, `.gb-fill` (+ `.neg`/`.pos`), `.gb-c`, `.gest-stats`, `.gest-themes`, `.gest-avis`, `.gest-citation`, `.hiver-head`, `.hiver-score`, `.hiver-sur`, `.hiver-detail`, `.hd-crit`, `.hd-val`, `.hd-score` (existing reusable: zi-badge variants, kv/cell/k/v, fine, f-bloc)\n- [pending] Item 10: curl tests of new endpoints (rollup already done)\n- [pending] Item 11: frontend build (`cd frontend && npm run build` presumably), `pm2 restart lou-ka-web lou-ka-sync`, healthcheck\n- [pending] Item 12: commit + push origin main\n\n8. Current Work:\nJust completed the Listing.tsx integration (item 9, in_progress). The last three edits were to `frontend/src/pages/Listing.tsx`: (1) added imports `CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc, HiverScore`; (2) inserted into column B after PriceAnalysis:\n```tsx\n\n\n{l.immeuble && }\n\n```\n(3) after KaScoresBlock: `{l.hiver && }`. All five components and api.ts types/fetchers are written. The CSS for the new component classes has NOT been written yet — that is the immediately remaining piece of item 9.\n\n9. Optional Next Step:\nFinish todo item 9 (\"Frontend : CoutReel, HistoriqueLouka, ImmeubleBloc, GestionnaireBloc + Listing.tsx + api.ts + CSS\") by appending the missing styles to `frontend/src/styles.css` for all new classes listed in Pending Tasks (cr-*, st-pill/st-*, hl-*, im-*, gest-*/gb-*, hiver-*/hd-*), consistent with existing design tokens (`var(--grey)`, `var(--navy)`, `var(--ink-2/3)`, `var(--line)`, `var(--r-ctl)`, zi-badge palette). Then proceed to item 10/11: TypeScript build check + `npm run build` in `frontend/`, restart the running server (`pm2 restart lou-ka-web`), curl-test the new endpoints live (`/api/listings/{uid}/historique`, `/cout-reel`, `/recyclees`, `/api/immeuble?bkey=`, `/api/managers/logisco`, and embedded `immeuble`/`hiver` in `/api/listings/{uid}`), healthcheck, then item 12 commit + push (ensuring `.env` stays untracked and the SerpApi key appears nowhere in the diff).\n\nIf you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/simon-pierreboucher/.claude/projects/-Users-simon-pierreboucher-apps-lou-ka/62f13d0b-8c21-4252-973c-9bb84aed0233.jsonl\nContinue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with \"I'll continue\" or similar. Pick up the last task as if the break never happened."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f7edf12a-8273-42bf-82a4-0efc20e58d4a","timestamp":"2026-08-24T22:14:47.170Z","isSynthetic":true}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLvvScHhJvXGsgLPkgs","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISkgQKpwEIERgCKkALSL1zcWUkWeupadw+/ESFfhjkj+xphfxFQM3RHGZG0sWVjCJFVeaWRTIgz6oVVmYCE1kAfPWCzeyZzkIDzbgIMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHbhLPUBrABAhIMKaDPMH8mbxmJZiBUGgxKtOKxitIMxO1On3EiMEefrKMJAAurKzVAZphlfnMULLhrbqbbnPSdx8q1efxOsamakZeWWSSYmAcwR5OeLyqXAgAp2jxpF/z/Yac8vXfoVnKzV1NvYvq2qTlvROtibNkaHicjq44uuwqW3dDOOw1GdvygdR0R0FcjhDP9B3sAHLlWOnBqJ6pPt0b9882OTOdFs1JwHNrrnbkB7mMce8HxLPA4F8axNkFIWTRB0mD9avlgWsu0d9s5iUpbInsOZDPPnIPh6CFvqOCNNeCrkKJKiMpOciv7wWHzSykOs61f1uddixjBqGspiYDW2NyG9R16vZTiwZwR2a/zsWgvhJIdHMkqhfUuhQloeq4MjnZZCbew2+o7v/2kx+2gGJIQ8oi8A1dHCjlPwn9wVfpZAchmjdEgqvNeaAplPAVXB15fs4YjdyZn+m605URzpJZoufD5dxGoagwarBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":27429,"cache_read_input_tokens":21718,"cache_creation":{"ephemeral_5m_input_tokens":27429,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"06315831-2c0f-4c8e-8d09-12506d648d47"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLvvScHhJvXGsgLPkgs","type":"message","role":"assistant","content":[{"type":"text","text":"Il reste le CSS des nouveaux blocs. Je regarde les tokens existants dans styles.css pour rester cohérent."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":27429,"cache_read_input_tokens":21718,"cache_creation":{"ephemeral_5m_input_tokens":27429,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"dbd0903c-f223-47c4-8b98-49f8b804eac0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLvvScHhJvXGsgLPkgs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01B1GxwcJ7AsNRFp6RWZLswq","name":"Grep","input":{"pattern":"st-pill|cr-table|hl-timeline|gest-|hiver-","path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":27429,"cache_read_input_tokens":21718,"cache_creation":{"ephemeral_5m_input_tokens":27429,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"11cb411a-0d95-4cf1-abf4-f5b41562ad6f"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01B1GxwcJ7AsNRFp6RWZLswq","type":"tool_result","content":"No matches found"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a69109eb-f8c6-4800-80a4-68f88a4f3034","timestamp":"2026-08-24T22:14:53.475Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"","numLines":0}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLvvScHhJvXGsgLPkgs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012SXQqBMLFSX65wG7VvzJ9s","name":"Bash","input":{"command":"tail -80 /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css; wc -l /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","description":"Show end of styles.css and line count"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":27429,"cache_read_input_tokens":21718,"cache_creation":{"ephemeral_5m_input_tokens":27429,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"af697371-ae3c-4cd4-a372-966ab940811f"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012SXQqBMLFSX65wG7VvzJ9s","type":"tool_result","content":"}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n 2115 /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f8820215-79dc-4633-afe5-d2844b70d03d","timestamp":"2026-08-24T22:14:54.744Z","tool_use_result":{"stdout":"}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n 2115 /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"62e84548-7a96-459f-bd7f-2f1bf91e03fb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLwUjp3yctVdVaybXp1","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS3QMKpwEIERgCKkAgzf3s8oI4PHxHCQ/7032RB2s0S3jbpfC1p8cy7mhCaoAAcbh+H0pwenr6IVDFtQrS5kOEPz+c2vMaUp6PdTfzMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHihLPUBrABAhIMu5x09dtBzw6fSC2xGgz8A2+4KIhqQyh5d3AiMAp8cxt/yyii+NWEFHjzQsb1yv3FG6ICIZ7dZYXSI3GQLp+qnKZqmXGak6E9pdCLISriAYhjsH6ycy3o9HABo2XOb/xyZa/oW2O3eTI2tLnNQ4yiMudSAC6d2ZmvZ3pOD3bvnsMzm3HsPhGqHSGl9Q4F5ehgYWYqnCnuP2os+HQg45uQ6nvABeSdf9xVWKZCZWyr3cejKZKRxrFOMF7rklgTO9uFAZUAPpfTRTtgU8mSUaxSE365mLlRyj7ISwwV3ksO8YmASk3jtHnLpSWphM3Stg0EG+zYqfDbm4pSCTFpIN4sD8pDQlyCrIUp8htmfKXpOKtwc81t1sQ4AneibN5Vi3MYWOOMmdZ/O91TFphWMOS9hpwYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1997,"cache_read_input_tokens":49147,"cache_creation":{"ephemeral_5m_input_tokens":1997,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e969262c-575d-4865-b620-1cb226298de1"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLwUjp3yctVdVaybXp1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FjwkFK1N2hkSbV7AfyfnrK","name":"Grep","input":{"pattern":"--ink|--surface|--accent|--font-|--r-ctl|--line","path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","output_mode":"content","head_limit":30},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1997,"cache_read_input_tokens":49147,"cache_creation":{"ephemeral_5m_input_tokens":1997,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7f27501c-3cf5-4747-9643-7860c8c8673e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FjwkFK1N2hkSbV7AfyfnrK","type":"tool_result","content":"16: --accent: #ff6a00;\n17: --accent-soft: #fff1e6;\n18: --accent-deep: #cc5500;\n20: --lime: var(--accent);\n21: --lime-soft: var(--accent-soft);\n25: --navy: var(--ink);\n26: --navy-soft: var(--surface-2);\n29: --orange: var(--accent);\n30: --orange-soft: var(--accent-soft);\n31: --orange-deep: var(--accent-deep);\n32: --grey: var(--surface-2);\n40:.mono { font-family: var(--font-mono); }\n51: border-bottom: 1px solid var(--line);\n55: font-family: var(--font-display); font-weight: 700; font-size: 25px;\n62: font-size: 11px; font-weight: 500; color: var(--ink-3);\n98:.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n101: background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n103: color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n135: background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n146: padding: 0 15px; min-height: 42px; color: var(--ink-2);\n155: font-size: 14px; color: var(--ink); font-family: inherit;\n158: border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n164: gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n171: text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n174: border: none; background: transparent; outline: none; font-family: var(--font-display);\n183:.range-sep { color: var(--ink-3); font-size: 11px; }\n185: flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n186: background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n187: font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n196: border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n\n[Showing results with pagination = limit: 30]"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2720ad7d-61fe-49e8-b67e-fe75177b6824","timestamp":"2026-08-24T22:15:00.175Z","tool_use_result":{"mode":"content","numFiles":0,"filenames":[],"content":"16: --accent: #ff6a00;\n17: --accent-soft: #fff1e6;\n18: --accent-deep: #cc5500;\n20: --lime: var(--accent);\n21: --lime-soft: var(--accent-soft);\n25: --navy: var(--ink);\n26: --navy-soft: var(--surface-2);\n29: --orange: var(--accent);\n30: --orange-soft: var(--accent-soft);\n31: --orange-deep: var(--accent-deep);\n32: --grey: var(--surface-2);\n40:.mono { font-family: var(--font-mono); }\n51: border-bottom: 1px solid var(--line);\n55: font-family: var(--font-display); font-weight: 700; font-size: 25px;\n62: font-size: 11px; font-weight: 500; color: var(--ink-3);\n98:.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n101: background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n103: color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n135: background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n146: padding: 0 15px; min-height: 42px; color: var(--ink-2);\n155: font-size: 14px; color: var(--ink); font-family: inherit;\n158: border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n164: gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n171: text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n174: border: none; background: transparent; outline: none; font-family: var(--font-display);\n183:.range-sep { color: var(--ink-3); font-size: 11px; }\n185: flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n186: background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n187: font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n196: border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;","numLines":30,"appliedLimit":30}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"e20a4ff2-a84f-4a4c-ab6b-347494a71900","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLwt4GG2dmMUogyhdtj","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS8QIKpwEIERgCKkA162Xb3ReYJu3BzK8kk9pn3aIMwgee4Hd/pKOoMa7A0UnJjThcTXvLdDXqPls5htdT6tacFM34UT+Q4kTQXoXkMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHohLPUBrABAhIMdECbBCfhkc0qLjIrGgz4a3a8IP15ogJYxNAiMGdM5yORbRm0r0e/gEtYIrnIR54K6aBLzWAoih6jOqZEcrjZ3IrgEuzxlmEd2x9cTip3g1vLYJkznExU3dlL43VOluNKdWiofaVTv4lmfuxH9h975GnKvt0fBDhp5HULT/B4BzouZo45U8xw9rPT/n7Flve2I5o3yAdPU+pKfFg9XU4LZ7+vFLRQmMK5oQIcy8hXbL0qC9/nKqPArL7UGlmzOWu4xob+rhgYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1123,"cache_read_input_tokens":51144,"cache_creation":{"ephemeral_5m_input_tokens":1123,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c4f32343-038f-43ee-846e-e33732583f73"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLwt4GG2dmMUogyhdtj","type":"message","role":"assistant","content":[{"type":"text","text":"J'ajoute maintenant le CSS des nouveaux blocs à la fin de styles.css, aligné sur les tokens existants."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1123,"cache_read_input_tokens":51144,"cache_creation":{"ephemeral_5m_input_tokens":1123,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"424ef3cf-a67d-4de4-8cfb-e55aeddb15d6"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLwt4GG2dmMUogyhdtj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QDnqaqFZasivbMvLpBtBy7","name":"Bash","input":{"command":"cat >> /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css <<'EOF'\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1px solid currentColor; border-radius: var(--r-pill, 999px);\n white-space: nowrap;\n}\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--line);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\nEOF\nwc -l /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","description":"Append CSS for new intelligence-dossier blocks to styles.css"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1123,"cache_read_input_tokens":51144,"cache_creation":{"ephemeral_5m_input_tokens":1123,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8f4b6e71-655d-4119-8b36-8a02b4f9e8c2"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QDnqaqFZasivbMvLpBtBy7","type":"tool_result","content":" 2281 /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"62572476-b5a0-4bbf-9111-0543ecbb9107","timestamp":"2026-08-24T22:15:35.804Z","tool_use_result":{"stdout":" 2281 /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"eef41e76-00f5-433c-950b-7995ee53ce1e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLzWP7Xvjq1EKxxXsRW","type":"message","role":"assistant","content":[{"type":"text","text":"CSS ajouté (166 lignes). Je lance le build frontend."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3619,"cache_read_input_tokens":52267,"cache_creation":{"ephemeral_5m_input_tokens":3619,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"99f7eb68-5ba3-4e6f-bf32-7dfb4303b133"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNLzWP7Xvjq1EKxxXsRW","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Qf8XpE7tm1Co6cQdHq71vq","name":"Bash","input":{"command":"cd /Users/simon-pierreboucher/apps/lou-ka/frontend && npm run build 2>&1 | tail -20","description":"Build frontend (tsc + vite)","timeout":300000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":3619,"cache_read_input_tokens":52267,"cache_creation":{"ephemeral_5m_input_tokens":3619,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4766f5de-d4f3-4233-8bb6-bb61480afa61"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"blw6brbca","tool_use_id":"toolu_01Qf8XpE7tm1Co6cQdHq71vq","description":"Build frontend (tsc + vite)","task_type":"local_bash","uuid":"1bdfba01-6703-457f-a314-dbdd3f8551b9","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"blw6brbca","tool_use_id":"toolu_01Qf8XpE7tm1Co6cQdHq71vq","status":"completed","output_file":"","summary":"Build frontend (tsc + vite)","uuid":"3caa5369-a634-4f4d-8c62-77202939bcf6","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Qf8XpE7tm1Co6cQdHq71vq","type":"tool_result","content":"rendering chunks...\ncomputing gzip size...\ndist/index.html 2.18 kB │ gzip: 0.89 kB\ndist/assets/config-DZiIPMGw.css 46.45 kB │ gzip: 6.71 kB\ndist/assets/index-KBNHCTbq.css 97.26 kB │ gzip: 18.70 kB\ndist/assets/CtFicheMap-CpoN1cky.js 0.81 kB │ gzip: 0.51 kB\ndist/assets/MetroLignes-Ey-hG4--.js 0.91 kB │ gzip: 0.44 kB\ndist/assets/KaSpotlightMap-CFixTB1M.js 0.97 kB │ gzip: 0.56 kB\ndist/assets/ListingMap3D-vWXJSdLv.js 1.02 kB │ gzip: 0.62 kB\ndist/assets/PropertyPreview-D7RGTAjF.js 1.51 kB │ gzip: 0.81 kB\ndist/assets/CtMapView-BGhyVcu3.js 3.13 kB │ gzip: 1.61 kB\ndist/assets/MapSearch-BfcKK_ZU.js 15.72 kB │ gzip: 5.86 kB\ndist/assets/index-DT4UQ8wG.js 392.65 kB │ gzip: 116.31 kB\ndist/assets/config-B8pv2bYP.js 1,899.14 kB │ gzip: 529.98 kB\n\n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.\n✓ built in 2.85s","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3a1aa135-c167-4172-9f30-63e4cc412dce","timestamp":"2026-08-24T22:15:46.131Z","tool_use_result":{"stdout":"rendering chunks...\ncomputing gzip size...\ndist/index.html 2.18 kB │ gzip: 0.89 kB\ndist/assets/config-DZiIPMGw.css 46.45 kB │ gzip: 6.71 kB\ndist/assets/index-KBNHCTbq.css 97.26 kB │ gzip: 18.70 kB\ndist/assets/CtFicheMap-CpoN1cky.js 0.81 kB │ gzip: 0.51 kB\ndist/assets/MetroLignes-Ey-hG4--.js 0.91 kB │ gzip: 0.44 kB\ndist/assets/KaSpotlightMap-CFixTB1M.js 0.97 kB │ gzip: 0.56 kB\ndist/assets/ListingMap3D-vWXJSdLv.js 1.02 kB │ gzip: 0.62 kB\ndist/assets/PropertyPreview-D7RGTAjF.js 1.51 kB │ gzip: 0.81 kB\ndist/assets/CtMapView-BGhyVcu3.js 3.13 kB │ gzip: 1.61 kB\ndist/assets/MapSearch-BfcKK_ZU.js 15.72 kB │ gzip: 5.86 kB\ndist/assets/index-DT4UQ8wG.js 392.65 kB │ gzip: 116.31 kB\ndist/assets/config-B8pv2bYP.js 1,899.14 kB │ gzip: 529.98 kB\n\n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.\n✓ built in 2.85s","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8149c80f-2127-4d69-b267-6b64dd0eadec","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM1GPmfDuUtESF7hhDN","type":"message","role":"assistant","content":[{"type":"text","text":"Build OK (TypeScript + Vite passent). Je redémarre l'app et je teste les nouveaux endpoints en live."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":826,"cache_read_input_tokens":55886,"cache_creation":{"ephemeral_5m_input_tokens":826,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"df70baa6-c529-47c7-a8c5-57727760969f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM1GPmfDuUtESF7hhDN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HqLndNLr2xQi1NgLC6TmiZ","name":"Bash","input":{"command":"pm2 restart lou-ka-web --update-env 2>&1 | tail -3 && sleep 2 && curl -sf http://localhost:8095/api/health 2>/dev/null || curl -s http://localhost:8095/api/stats | head -c 300","description":"Restart lou-ka-web and healthcheck"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":826,"cache_read_input_tokens":55886,"cache_creation":{"ephemeral_5m_input_tokens":826,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9ccb12f9-9760-45ed-8f53-f15dab86d4dd"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HqLndNLr2xQi1NgLC6TmiZ","type":"tool_result","content":"│ 23 │ voyage2026 │ default │ N/A │ fork │ 43917 │ 18D │ 0 │ online │ 0% │ 40.4mb │ simon-p… │ disabled │\n│ 24 │ voyage2026-ngrok │ default │ N/A │ fork │ 43929 │ 18D │ 0 │ online │ 0% │ 61.9mb │ simon-p… │ disabled │\n└────┴────────────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘\n{\"total\":45731,\"quebec\":3430,\"levis\":1375,\"montreal\":23123,\"autres\":17803,\"sources\":189,\"avg_price\":1810.133327939472,\"quality\":{\"avg_completeness\":91.4,\"publiees\":59935,\"quarantaine\":1181},\"recent_syncs\":[{\"id\":39104,\"source\":\"immobilia\",\"ts\":1787609537.738507,\"found\":0,\"added\":0,\"updated\":0,\"remov","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"95a4fa5c-1e00-41a8-bc60-f47792505395","timestamp":"2026-08-24T22:15:54.141Z","tool_use_result":{"stdout":"│ 23 │ voyage2026 │ default │ N/A │ fork │ 43917 │ 18D │ 0 │ online │ 0% │ 40.4mb │ simon-p… │ disabled │\n│ 24 │ voyage2026-ngrok │ default │ N/A │ fork │ 43929 │ 18D │ 0 │ online │ 0% │ 61.9mb │ simon-p… │ disabled │\n└────┴────────────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘\n{\"total\":45731,\"quebec\":3430,\"levis\":1375,\"montreal\":23123,\"autres\":17803,\"sources\":189,\"avg_price\":1810.133327939472,\"quality\":{\"avg_completeness\":91.4,\"publiees\":59935,\"quarantaine\":1181},\"recent_syncs\":[{\"id\":39104,\"source\":\"immobilia\",\"ts\":1787609537.738507,\"found\":0,\"added\":0,\"updated\":0,\"remov","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6efc217b-a55f-4445-8656-aee01bc8fcfc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM1rWc4BDtbNJFZfh2B","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS6wMKpwEIERgCKkDIeyXDaoAfeAAJ9/EYAgOPtKaYMEsRg3Gl0dlwkmmPhrzQqaAxLXHo61OMqY45vR4qWGv3oHXy3vRSAH4iSbwzMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGihbPUBrABAhIMe1B4yPKt9ZnAw3tTGgzsfo5N1dbnJb43gxwiMMXtcZTkfYT+uhCAemUedwPn9BWJFMmZJsd92nbGxxhpNiqi6ZuzdT0tYbKWTOV01irwAePVGNWkGij7uwEjEXcwwt1vEP2fgTUnBPdm3uUpxkwSLBxcd70IjyGDrCsP9xYV1ulepwd4ivWa3CEDj2FJ2van8JgI+c1rdDQmenG0pQWTY+MYa7Xx/pnTeKwJ9rzCHryKiB+IBMG05lk8wVTZCbxOodkC6cwkXIe1HRpIFJBxsv3kXhekQjkZuzsYZxfr/wd5HbYaZWKoDkhJUoW4nVa3m+kStzFJlUlo+Jzh2e37oAmGVBciCXjHEDB3rjeOKyk/4M/8oOAeLqTdp61f3PwGY/e2+zZcpmT9Ppl1Euq3lYpy2AdCi0QHDLRv3lKu+RgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1157,"cache_read_input_tokens":56712,"cache_creation":{"ephemeral_5m_input_tokens":1157,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1fd158a1-b680-453d-b7b2-1f1fe9de9193"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM1rWc4BDtbNJFZfh2B","type":"message","role":"assistant","content":[{"type":"text","text":"L'app répond. Je teste les cinq nouveaux endpoints en live."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1157,"cache_read_input_tokens":56712,"cache_creation":{"ephemeral_5m_input_tokens":1157,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fa8a29de-b114-480b-bccf-be2ed3588b7f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM1rWc4BDtbNJFZfh2B","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016qH489zxjPWCFuiH1vHFfW","name":"Bash","input":{"command":"cd /Users/simon-pierreboucher/apps/lou-ka && UID_T=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nr = con.execute(\\\"SELECT uid FROM listings WHERE source='app_urbains' AND uid LIKE '%archipel%' LIMIT 1\\\").fetchone()\nprint(r[0] if r else '')\ncon.close()\") && echo \"uid=$UID_T\" && for ep in historique cout-reel recyclees; do echo \"--- $ep:\"; curl -s \"http://localhost:8095/api/listings/$UID_T/$ep\" | head -c 400; echo; done","description":"Curl-test historique, cout-reel, recyclees endpoints"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1157,"cache_read_input_tokens":56712,"cache_creation":{"ephemeral_5m_input_tokens":1157,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"21490e25-3086-4444-9532-dacb908b1ce5"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016qH489zxjPWCFuiH1vHFfW","type":"tool_result","content":"uid=app_urbains:archipel-112\n--- historique:\n{\"uid\":\"app_urbains:archipel-112\",\"premiere_observation\":1786031643.631547,\"derniere_observation\":1786969078.066277,\"active\":false,\"jours_en_ligne\":11,\"prix_initial\":null,\"prix_actuel\":1315.0,\"variation\":null,\"modifications\":0,\"timeline\":[],\"methode\":\"Observations directes des synchronisations Lou-Ka (prix, description, superficie, disponibilité, inclusions, photos, retraits/retours). Aucune esti\n--- cout-reel:\n{\"uid\":\"app_urbains:archipel-112\",\"version\":\"cout-reel-1.0\",\"loyer\":1315.0,\"lignes\":[{\"poste\":\"Loyer affiché\",\"statut\":\"observed\",\"montant\":1315.0,\"source\":\"annonce\"},{\"poste\":\"Électricité\",\"statut\":\"unknown\",\"montant\":null,\"source\":null,\"note\":\"l'annonce ne précise pas si elle est incluse\"},{\"poste\":\"Chauffage\",\"statut\":\"unknown\",\"montant\":null,\"note\":\"ni inclus ni estimable avec les données\n--- recyclees:\n{\"matches\":[],\"statut\":\"inferred\",\"methode\":\"similarité multi-signaux (unité, type, chambres, superficie, photos par hash, texte) parmi les annonces antérieures du même immeuble\"}","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"90700eb6-258f-48a8-b151-ecc0986300a2","timestamp":"2026-08-24T22:16:05.542Z","tool_use_result":{"stdout":"uid=app_urbains:archipel-112\n--- historique:\n{\"uid\":\"app_urbains:archipel-112\",\"premiere_observation\":1786031643.631547,\"derniere_observation\":1786969078.066277,\"active\":false,\"jours_en_ligne\":11,\"prix_initial\":null,\"prix_actuel\":1315.0,\"variation\":null,\"modifications\":0,\"timeline\":[],\"methode\":\"Observations directes des synchronisations Lou-Ka (prix, description, superficie, disponibilité, inclusions, photos, retraits/retours). Aucune esti\n--- cout-reel:\n{\"uid\":\"app_urbains:archipel-112\",\"version\":\"cout-reel-1.0\",\"loyer\":1315.0,\"lignes\":[{\"poste\":\"Loyer affiché\",\"statut\":\"observed\",\"montant\":1315.0,\"source\":\"annonce\"},{\"poste\":\"Électricité\",\"statut\":\"unknown\",\"montant\":null,\"source\":null,\"note\":\"l'annonce ne précise pas si elle est incluse\"},{\"poste\":\"Chauffage\",\"statut\":\"unknown\",\"montant\":null,\"note\":\"ni inclus ni estimable avec les données\n--- recyclees:\n{\"matches\":[],\"statut\":\"inferred\",\"methode\":\"similarité multi-signaux (unité, type, chambres, superficie, photos par hash, texte) parmi les annonces antérieures du même immeuble\"}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"22ca614c-543f-4a4d-91f1-531347efdfd0","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM2hSgYintx2EnSsTip","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01M9EGzWcucBR3j3dzToup2t","name":"Bash","input":{"command":"echo \"--- immeuble:\"; curl -s \"http://localhost:8095/api/immeuble?bkey=adr%3A7070%7Cguillaume-couture%7Clevis\" | head -c 400; echo; echo \"--- manager logisco:\"; curl -s \"http://localhost:8095/api/managers/logisco\" | grep -v cede959 | head -c 600; echo; echo \"--- embedded immeuble/hiver:\"; curl -s \"http://localhost:8095/api/listings/app_urbains:archipel-112\" | .venv/bin/python -c \"import json,sys; d=json.load(sys.stdin); print('immeuble:', json.dumps(d.get('immeuble'))[:200]); print('hiver:', json.dumps(d.get('hiver'))[:200])\"","description":"Curl-test immeuble, manager, embedded fields"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":889,"cache_read_input_tokens":57869,"cache_creation":{"ephemeral_5m_input_tokens":889,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0d7156b8-8e12-42ea-bb30-66ae2d379940"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01M9EGzWcucBR3j3dzToup2t","type":"tool_result","content":"--- immeuble:\n{\"bkey\":\"adr:7070|guillaume-couture|levis\",\"address\":\"7070 Bd Guillaume-Couture, Lévis, QC G6V 0R9\",\"city\":\"Lévis\",\"version\":\"immeuble-1.0\",\"computed_at\":1787609278.383887,\"annonces_total\":14,\"annonces_actives\":9,\"annonces_30j\":14,\"annonces_90j\":14,\"annonces_12m\":14,\"unites_identifiees\":0,\"unites_estimees\":9,\"loyer_median\":1650,\"loyer_median_par_cc\":{\"1\":1380,\"2\":1700,\"3\":1820},\"pi2_median\":1.41\n--- manager logisco:\n{\"source_id\":\"logisco\",\"nom\":\"Logisco\",\"site_web\":\"https://logisco.com/fr\",\"telephone\":\"+1 833-564-4726\",\"annonces_actives\":159,\"version\":\"gestionnaire-1.0\",\"google_maps\":{\"nom\":\"Logisco - Siège social\",\"adresse\":\"950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7\",\"note\":3.6,\"nombre_avis\":395,\"place_id\":\"ChIJK8TdTNWTuEwR2VoogCXlNu8\",\"confiance_association\":1.0,\"signaux\":{\"similarite_nom\":1.0,\"meme_domaine_web\":\"logisco.com\",\"ville_correspondante\":\"Québec\",\"fiche_entreprise\":\"Agence immobilière\",\"type_fiche\":\"Agence immobilière\",\"nombre_avis\":395},\"statut\":\"inferred\",\"methode\":\"fiche\n--- embedded immeuble/hiver:\nimmeuble: {\"bkey\": \"adr:7070|guillaume-couture|levis\", \"address\": \"7070 Bd Guillaume-Couture, L\\u00e9vis, QC G6V 0R9\", \"city\": \"L\\u00e9vis\", \"version\": \"immeuble-1.0\", \"computed_at\": 1787609278.383887, \"annonce\nhiver: {\"score\": 41, \"classe\": \"exigeant\", \"detail\": [{\"critere\": \"\\u00c9picerie\", \"score\": 2, \"distance_m\": 1132, \"minutes\": 18, \"nom\": \"Maxi\"}, {\"critere\": \"Pharmacie\", \"score\": 59, \"distance_m\": 613, \"min","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bdd1954f-459c-4a24-81f5-e00e4f2d3151","timestamp":"2026-08-24T22:16:11.272Z","tool_use_result":{"stdout":"--- immeuble:\n{\"bkey\":\"adr:7070|guillaume-couture|levis\",\"address\":\"7070 Bd Guillaume-Couture, Lévis, QC G6V 0R9\",\"city\":\"Lévis\",\"version\":\"immeuble-1.0\",\"computed_at\":1787609278.383887,\"annonces_total\":14,\"annonces_actives\":9,\"annonces_30j\":14,\"annonces_90j\":14,\"annonces_12m\":14,\"unites_identifiees\":0,\"unites_estimees\":9,\"loyer_median\":1650,\"loyer_median_par_cc\":{\"1\":1380,\"2\":1700,\"3\":1820},\"pi2_median\":1.41\n--- manager logisco:\n{\"source_id\":\"logisco\",\"nom\":\"Logisco\",\"site_web\":\"https://logisco.com/fr\",\"telephone\":\"+1 833-564-4726\",\"annonces_actives\":159,\"version\":\"gestionnaire-1.0\",\"google_maps\":{\"nom\":\"Logisco - Siège social\",\"adresse\":\"950 Rue de la Concorde Bureau 302, Levis, Quebec G6W 8A7\",\"note\":3.6,\"nombre_avis\":395,\"place_id\":\"ChIJK8TdTNWTuEwR2VoogCXlNu8\",\"confiance_association\":1.0,\"signaux\":{\"similarite_nom\":1.0,\"meme_domaine_web\":\"logisco.com\",\"ville_correspondante\":\"Québec\",\"fiche_entreprise\":\"Agence immobilière\",\"type_fiche\":\"Agence immobilière\",\"nombre_avis\":395},\"statut\":\"inferred\",\"methode\":\"fiche\n--- embedded immeuble/hiver:\nimmeuble: {\"bkey\": \"adr:7070|guillaume-couture|levis\", \"address\": \"7070 Bd Guillaume-Couture, L\\u00e9vis, QC G6V 0R9\", \"city\": \"L\\u00e9vis\", \"version\": \"immeuble-1.0\", \"computed_at\": 1787609278.383887, \"annonce\nhiver: {\"score\": 41, \"classe\": \"exigeant\", \"detail\": [{\"critere\": \"\\u00c9picerie\", \"score\": 2, \"distance_m\": 1132, \"minutes\": 18, \"nom\": \"Maxi\"}, {\"critere\": \"Pharmacie\", \"score\": 59, \"distance_m\": 613, \"min","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"1e6763a2-9201-499c-b972-e67dcf1d3d4d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM37uqpmkQeptMoXGqy","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS4wYKpwEIERgCKkBWr8NA0kcWCjXGvGd+IVxnN+b9/YInTM/vSqefvhkAKfHH2OWxVEGaB6IPR5rjHYZohdHfO/qKaUGfnkYHBfTTMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGyhbPUBrABAhIMXttH5A8YL/Mk/wbjGgx9yKsZgKJQlWI5SCIiMAz0CJGhHMEBDKOgGAkCRUL4sx2SmyC0od4CWwPrJbfUgSBpKqc18oMvQGeYw5ABuCroBByXzQ9at+h/7JivN03sveo4p7vMOa4xpI+/KHEkum0Cao1dLyAHfUmpeMtX77Kn8FH7+hDkCV8teB8FIapK5IhyyQd0zzuFQgnPLz09zJvex4ZjvTx/vlnDikosxQMies5qEFDbmP8ZOFQBM2ixuPtxA8hiWN5B4nF+fxSHIqtgE1xRUsOKFXAjL3Vh4t5aTyMjepU79sMYZ8kmX3ZYjpC+0uV8RYwRckZk9xV4c8mr22eAu7DIMT/gVgX2aJ+OpTJFXyxvLpg3HZfoUPaS1alpXC+2MeS0TjNahGsu8zC+0fwVj1SjUHCUeLU0XEGyRhyvK2p8b29E7Ujl5ZtnDZYMfUREvwdR+LIGzKZYpO18W/wnty9s5mxgW5LpIZSWZHqc7bldYWZFhHPHw2r4ZMq/LemJjcttLUuI1MI+SF02QJt86aTAB/CHJupJK0SNKdg4inQewrMaJbZLtfblVYY1xSNiFj93vVLEu5RFtBMNmHgM+SYMsvUte/oFIAN9sEgP7dQbRuGHn76bKPgiavvckpSVTreI6IzohZpKBhwxeed3TOAG3MwKhQjWZg7iJCzcCA4FxQhiU1BkcLRqdwfYLee8584SlpHbCk/934zCyiZHnNGWA480l1Qyy+iW3W8jKiqWaCNy/B8OXoKCVSL4ss0yY9dV3VC9gjwCNa+WAIOc4ks+1NHxnPHEUqF8S0EI6d68kxyDMtJ/C+G2FRCUfjor3BbYPMeXIXXVLJUuL3OWf5R/o/3zDtmwC7wFB+139GJQ80/yrqHsmKni/3ZHTtwXufbIErliLngbRzg3AyFeADJeXuwYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":58758,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5d4f4519-d9af-4ce1-9275-dc529145a5d7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM37uqpmkQeptMoXGqy","type":"message","role":"assistant","content":[{"type":"text","text":"Tous les endpoints répondent correctement. Je redémarre aussi `lou-ka-sync` (ingest.py modifié) et je valide l'ordre visuel de la fiche avec le script Playwright."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":58758,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7c048015-9d11-4d5e-a984-577b926b7ce7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM37uqpmkQeptMoXGqy","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012S7ZKARCd3G52AgrK542Vd","name":"Bash","input":{"command":"pm2 restart lou-ka-sync --update-env 2>&1 | grep -E \"lou-ka-sync|online\" | head -2; pm2 list | grep lou-ka","description":"Restart lou-ka-sync and check PM2 status"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":58758,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5f28d7e3-7d26-46dd-a84d-ed5cd078913c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012S7ZKARCd3G52AgrK542Vd","type":"tool_result","content":"[PM2] Applying action restartProcessId on app [lou-ka-sync](ids: [ 26 ])\n[PM2] [lou-ka-sync](26) ✓\n│ 27 │ lou-ka-ngrok │ default │ N/A │ fork │ 22790 │ 18D │ 0 │ online │ 0% │ 349.8mb │ simon-p… │ disabled │\n│ 26 │ lou-ka-sync │ default │ 1.0.0 │ fork │ 68086 │ 0s │ 42 │ online │ 0% │ 42.6mb │ simon-p… │ disabled │\n│ 25 │ lou-ka-web │ default │ 1.0.0 │ fork │ 66964 │ 28s │ 106 │ online │ 0% │ 128.6mb │ simon-p… │ disabled │","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"83994473-254b-4b03-a11f-3147ee3c60af","timestamp":"2026-08-24T22:16:20.289Z","tool_use_result":{"stdout":"[PM2] Applying action restartProcessId on app [lou-ka-sync](ids: [ 26 ])\n[PM2] [lou-ka-sync](26) ✓\n│ 27 │ lou-ka-ngrok │ default │ N/A │ fork │ 22790 │ 18D │ 0 │ online │ 0% │ 349.8mb │ simon-p… │ disabled │\n│ 26 │ lou-ka-sync │ default │ 1.0.0 │ fork │ 68086 │ 0s │ 42 │ online │ 0% │ 42.6mb │ simon-p… │ disabled │\n│ 25 │ lou-ka-web │ default │ 1.0.0 │ fork │ 66964 │ 28s │ 106 │ online │ 0% │ 128.6mb │ simon-p… │ disabled │","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM37uqpmkQeptMoXGqy","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Cp3LHwX28cvpKXVK9e8Kud","name":"Bash","input":{"command":"UID_A=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nr = con.execute(\\\"SELECT uid FROM listings WHERE active=1 AND source='app_urbains' AND uid LIKE '%archipel%' LIMIT 1\\\").fetchone()\nprint(r[0] if r else '')\ncon.close()\") && echo \"uid actif: $UID_A\" && node frontend/scripts/check-order.mjs \"$UID_A\" 2>&1 | tail -15","description":"Run visual-order check on an active listing","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":58758,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6b3f0690-5ba2-4366-9527-a7b8cfcf2921"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bl1ann97b","tool_use_id":"toolu_01Cp3LHwX28cvpKXVK9e8Kud","description":"Run visual-order check on an active listing","task_type":"local_bash","uuid":"f915b566-71f7-44a9-84ac-4af6544752be","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bl1ann97b","tool_use_id":"toolu_01Cp3LHwX28cvpKXVK9e8Kud","status":"completed","output_file":"","summary":"Run visual-order check on an active listing","uuid":"9887cb9e-fd35-435b-9a55-7595f100c171","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Cp3LHwX28cvpKXVK9e8Kud","type":"tool_result","content":"uid actif: app_urbains:archipel-217\n\n=== Desktop 1440px === scrollY initial: 0\n.f-galerie top= 167 left= 144 order=0\n.f-hero top= 737 left= 144 order=0\n.f-desc top= 1159 left= 144 order=0\n.f-incl top= 1406 left= 144 order=0\n.f-pratique top= 1668 left= 144 order=0\n.f-fairvalue top= 167 left= 864 order=0\n.f-carte top= 3342 left= 864 order=0\n.f-kascores top= 6534 left= 864 order=0\n.f-quartier top= 7515 left= 864 order=0\n.f-poi top= 8348 left= 864 order=0\n\nMOBILE: ordre visuel CROISSANT ✓ · scrollY=0 ✓ · 1re section=.f-galerie top=145 · sans order CSS=true\nVALIDATION OK","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6f258654-a1bb-46b2-b1b6-c74ea08a3dd4","timestamp":"2026-08-24T22:16:32.590Z","tool_use_result":{"stdout":"uid actif: app_urbains:archipel-217\n\n=== Desktop 1440px === scrollY initial: 0\n.f-galerie top= 167 left= 144 order=0\n.f-hero top= 737 left= 144 order=0\n.f-desc top= 1159 left= 144 order=0\n.f-incl top= 1406 left= 144 order=0\n.f-pratique top= 1668 left= 144 order=0\n.f-fairvalue top= 167 left= 864 order=0\n.f-carte top= 3342 left= 864 order=0\n.f-kascores top= 6534 left= 864 order=0\n.f-quartier top= 7515 left= 864 order=0\n.f-poi top= 8348 left= 864 order=0\n\nMOBILE: ordre visuel CROISSANT ✓ · scrollY=0 ✓ · 1re section=.f-galerie top=145 · sans order CSS=true\nVALIDATION OK","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"cec85ae3-db19-4dbd-955d-f33372451620","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM4h1CVNLre4ARpoMnw","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISuwkKpwEIERgCKkBkY83gMNK9uUKy8K+zddihl/FqiP9butOCewzjuwcLTtNA513oMueSEWPjlcCjLc0XDu1Nwde0fmPst81ruxgiMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHNhbPUBrABAhIMzOwJ72icdngsOKXKGgxqmbDX6zPI2T5MmU8iMM5kPh/QOm4Fv5Kkrp/wG4E6TgG5MMqfibE7gdt1t+7RZTjE/6U7RhQnC/V5PXrcsyrABz4/FEoj/N/rgN6kdXCXebWvbHUhf5UmwJ38StFrXgg7gyrUYCkyJVGXVLTv5EZMa4DVP3V3mWZT6JZn1RjPLq0Vo3TeMBe0Dw+Jf0O6QP1GaRFTydW85enu8dPrZvXrEdvW2JcsOLD+V82AjuvPI7kHF5vaPwYtT4VBkZ32gIH35L0t0X0iYmxwukCRGdXFnimvXSiYJgqIgMUSE308KlIKbO52YuSv9nY2gE7ygD/Ajv6ssGEkC5t1zN2fX74z+LxgXQRQKrulhpEnKPRpd5GmH6cpMVY1YTuQ91Kj5OyWeSuHYcOWWZmPgzP2Py6XuFTmckD42guwITiHFOb9yF0hr0vmxGA06BPEEA1zzhiH/8l6PyRrX8Tj7PWz1h+ExDY1qIrWuGDzzZh9nXYRdwBR7C9YLO33sua2eEIUIMPiBebYoOqpXPqlTSSSUeADtiHz/VRwKouZnJcXLZewA76VHvIRjEoR6tbQWGPY/qxVrlgbOqq7bibrcPYSQBqW8J19AVO5yrfCvCvBFnDKXA1+ELDol63PXPGoYTc1kPe1ls9fUrUXi1TOXwxXU/DpMVxowAwoGtbcuZzYYriB++xEosC5PE34Cv9yiNCGAVLsxq2q2pLWI17du7oPU8O80CVMNi3oq9bdxG2jNbH/8l0IlHizB2rl9QLUQNaUr3FOUtvckF95o8JC5ySIHJfvzIR8QE/vnHtCGhGxzQrM6HOZhfAPk3xdk5xKd4dpXZBV+6Nm+dGO7p8BeL7FQx55fodobxSlZhTpJc0Qa8+NiziBgWHu6Mn6YQRuqwhfLqavccls5JmUznnOKwzdh35iBIUFqXxD24ocLxE7Zgbj5Pr6BgZHGAQluoe9wxTiIf6NXJG96QqrofLmf18yyHHxVpJx3doQ1t55NyquAIMhwHMlJrLecdZzuzYn9MLQFq8EGQBaGRQGcvY2gYNvlYi01KhMfFlKU2lVwOi6wSae9gddKKlbBJ3pH9aA/5r0kDgqITOMgYZKaD86jEKOHOMHlX7mpL6ReI1GUuH+Ltbk31JxGXtAlZAxGYoKFhVxLR/rooODiWUASVE7M/3C+iNbh94M+vOqB2Qr1LzdQ1k11eYOWS/BjtejlpMCkY6pvR5aLGlpUkqMpsCztqP9Jeu+cI5uAshC+xpFARPS6Bz7389OoepJITASCDjmOthFkvOwzSh1PatmRYPTh/0/R/ONjJzhCUNPFTGt5AB5OEI5pKx63ByVvcm9b88E6Z0KL+jHn52bzP6I2rFJhZhtJvG6tRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1381,"cache_read_input_tokens":59878,"cache_creation":{"ephemeral_5m_input_tokens":1381,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8907ba06-8eb2-4e84-a9b6-0aeab2ce2ec9"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM4h1CVNLre4ARpoMnw","type":"message","role":"assistant","content":[{"type":"text","text":"Validation d'ordre OK sur desktop et mobile. Je vérifie rapidement que les nouveaux blocs s'affichent réellement dans le navigateur."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1381,"cache_read_input_tokens":59878,"cache_creation":{"ephemeral_5m_input_tokens":1381,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d0d44478-136f-4602-b4aa-65e919c7c3d9"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM4h1CVNLre4ARpoMnw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CxjmZb6sS4RqtFhY651rYV","name":"Bash","input":{"command":"grep -h 'className=\"f-bloc' frontend/src/components/CoutReel.tsx frontend/src/components/HistoriqueLouka.tsx frontend/src/components/ImmeubleBloc.tsx frontend/src/components/GestionnaireBloc.tsx frontend/src/components/HiverScore.tsx","description":"Get section class names of new blocks"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1381,"cache_read_input_tokens":59878,"cache_creation":{"ephemeral_5m_input_tokens":1381,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a85e2322-9216-4317-a886-fd6e3da4958b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CxjmZb6sS4RqtFhY651rYV","type":"tool_result","content":"
\n
\n
\n
\n
","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7e4be55e-5a61-46a3-a343-2812bc37b10a","timestamp":"2026-08-24T22:16:46.578Z","tool_use_result":{"stdout":"
\n
\n
\n
\n
","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"a8d42e05-0716-429e-9a3b-04d8eb6627d0","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM5jNr4BR8ptEmF2CsZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RD2Zp2Q6Cx7bhaTgcvJsUd","name":"Bash","input":{"command":"cd frontend && node -e \"\nconst { chromium } = require('playwright');\n(async () => {\n const b = await chromium.launch();\n const p = await b.newPage({ viewport: { width: 1440, height: 900 } });\n await p.goto('http://localhost:8095/logement/' + encodeURIComponent('app_urbains:archipel-217'), { waitUntil: 'networkidle' });\n await p.waitForTimeout(1500);\n for (const sel of ['.f-coutreel', '.f-histolk', '.f-immeuble', '.f-gest', '.f-hiver']) {\n const el = await p.\\$(sel);\n if (!el) { console.log(sel, 'ABSENT'); continue; }\n const box = await el.boundingBox();\n const txt = (await el.innerText()).replace(/\\s+/g, ' ').slice(0, 110);\n console.log(sel, box ? \\`top=\\${Math.round(box.y)}\\` : 'invisible', '|', txt);\n }\n await b.close();\n})();\" 2>&1","description":"Playwright check that all 5 new blocks render","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":697,"cache_read_input_tokens":61259,"cache_creation":{"ephemeral_5m_input_tokens":697,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"71b0978a-091f-4401-82e0-550dd72d6b74"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bhn57fxy1","tool_use_id":"toolu_01RD2Zp2Q6Cx7bhaTgcvJsUd","description":"Playwright check that all 5 new blocks render","task_type":"local_bash","uuid":"80b8cf60-73ab-45d4-b2b9-0d145f8bad4e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8f4a2d32-81f5-4c93-b6ed-fca04961c305","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bhn57fxy1","tool_use_id":"toolu_01RD2Zp2Q6Cx7bhaTgcvJsUd","status":"completed","output_file":"","summary":"Playwright check that all 5 new blocks render","uuid":"d70bf66c-375c-4ce1-aaa9-9e6d92910b08","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RD2Zp2Q6Cx7bhaTgcvJsUd","type":"tool_result","content":".f-coutreel top=679 | Coût réel mensuel Loyer affichéOBSERVÉ 1 380 $ ÉlectricitéINCONNU — ChauffageINCONNU — Eau chaudeINCONNU — Int\n.f-histolk top=1285 | Historique Lou-Ka SUIVIE DEPUIS 6 août 2026 EN LIGNE 18 jours MODIFICATIONS OBSERVÉES 0 Aucune modification ob\n.f-immeuble top=1708 | Passeport de l'immeuble ANNONCES OBSERVÉES 14 dont 9 actives UNITÉS ESTIMÉES ≥ 9 LOYER MÉDIAN (ACTIVES) 1 650 \n.f-gest ABSENT\n.f-hiver top=7070 | Vie quotidienne en hiver 41 EXIGEANT quotidien à pied, même à −20 °C ÉPICERIE ≈ 18 min à pied (Maxi) 2 PHARMAC","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cb586af3-74da-4531-ad3a-99766179a82e","timestamp":"2026-08-24T22:16:57.559Z","tool_use_result":{"stdout":".f-coutreel top=679 | Coût réel mensuel Loyer affichéOBSERVÉ 1 380 $ ÉlectricitéINCONNU — ChauffageINCONNU — Eau chaudeINCONNU — Int\n.f-histolk top=1285 | Historique Lou-Ka SUIVIE DEPUIS 6 août 2026 EN LIGNE 18 jours MODIFICATIONS OBSERVÉES 0 Aucune modification ob\n.f-immeuble top=1708 | Passeport de l'immeuble ANNONCES OBSERVÉES 14 dont 9 actives UNITÉS ESTIMÉES ≥ 9 LOYER MÉDIAN (ACTIVES) 1 650 \n.f-gest ABSENT\n.f-hiver top=7070 | Vie quotidienne en hiver 41 EXIGEANT quotidien à pied, même à −20 °C ÉPICERIE ≈ 18 min à pied (Maxi) 2 PHARMAC","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM6XjPg8mrYpZ2fqJG5","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS5QQKpwEIERgCKkCScWQvZr6ZwqV+PNc1224Hca29iOa+kmSbS+aZzgHrWPe1eT3Z7Xy0CVWbpolLTLiiEz0s2eRj1tFCIigGmyDiMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHfhbPUBrABAhIMlw17YO45iPbij6CpGgz1a3UYOBSBdGeNSwAiMOrrbV/uo/q1ZurSX7z9cp6UenTtFps2QZ7svPYGvSHp7jEK/R2I0WsRRkId+5xOlCrqAnL0mVjxFvzLkt061bM64aW4/6L8UyKZnGRGwn7voSNpL9WYtb+ASTfRoOsQ24NDpsMXOaeOonwvvH7Ji9E4s9jfUBfpfmJYLnm9XeFZyagfvItVAizH8RRp/8/WNiSfDKXgda1oy10Au9m+FYWxcxayOYa1s+wYL1k2gGI4VXAjeG7SUx4Z6CBojlDFmTGUHtKEi8hXn+tlU1zdIV6tF7psSwGzE4gN7FE2RVlcnRL2lBpIywAnb1TGMIScn1rj6tx5TLVBJdINor0b+JSoC26OBF1P3n+tOQIJubzU1BxwEgnmlC4NxlDVXzaW32d5lNy9D4fWmORYGkONGl9KUhAblrD88LcUlRV1JaciVQNFMjXZVPVXAhlSCHBCEpHqvBlmaxyGsPHBEVR+Heiw34BdaOEyAL3IvkY9F1T5AAGFjpTExoW30kpNUn9v4ZnairSozzzD8tpawz6cJzV/S57kAUFh8ihwHFxMGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":821,"cache_read_input_tokens":61956,"cache_creation":{"ephemeral_5m_input_tokens":821,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7ecfdc04-cf63-4818-925e-e6dd03ce3546"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM6XjPg8mrYpZ2fqJG5","type":"message","role":"assistant","content":[{"type":"text","text":"Quatre blocs s'affichent. `.f-gest` est absent pour `app_urbains` — attendu si la fiche Google n'est pas encore associée. Je confirme sur une annonce Logisco (gestionnaire résolu)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":821,"cache_read_input_tokens":61956,"cache_creation":{"ephemeral_5m_input_tokens":821,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c65a002f-a3ef-4c80-ade3-9a56fa154b81"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM6XjPg8mrYpZ2fqJG5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VaHvXgcE5Q8WRr4YFPSZY5","name":"Bash","input":{"command":"cd /Users/simon-pierreboucher/apps/lou-ka && UID_L=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nr = con.execute(\\\"SELECT uid FROM listings WHERE active=1 AND source='logisco' LIMIT 1\\\").fetchone()\nprint(r[0] if r else '')\ncon.close()\") && cd frontend && node -e \"\nconst { chromium } = require('playwright');\n(async () => {\n const b = await chromium.launch();\n const p = await b.newPage({ viewport: { width: 1440, height: 900 } });\n await p.goto('http://localhost:8095/logement/' + encodeURIComponent('$UID_L'), { waitUntil: 'networkidle' });\n await p.waitForTimeout(1500);\n const el = await p.\\$('.f-gest');\n if (!el) { console.log('.f-gest ABSENT'); } else {\n console.log('.f-gest OK |', (await el.innerText()).replace(/\\s+/g, ' ').slice(0, 250));\n }\n await b.close();\n})();\" 2>&1","description":"Check gestionnaire block on a Logisco listing","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":821,"cache_read_input_tokens":61956,"cache_creation":{"ephemeral_5m_input_tokens":821,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f2636507-bde0-4822-aa7a-a98f9588fdca"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"b9l0h8xi9","tool_use_id":"toolu_01VaHvXgcE5Q8WRr4YFPSZY5","description":"Check gestionnaire block on a Logisco listing","task_type":"local_bash","uuid":"741c9d0e-dc2a-447e-a893-e974e92cde06","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"b9l0h8xi9","tool_use_id":"toolu_01VaHvXgcE5Q8WRr4YFPSZY5","status":"completed","output_file":"","summary":"Check gestionnaire block on a Logisco listing","uuid":"721f577c-7be7-4f30-a59c-95f194698d92","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"0b442746-4310-4964-ac42-e59fc32a2edf","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VaHvXgcE5Q8WRr4YFPSZY5","type":"tool_result","content":".f-gest OK | Qui gère ce logement? Logisco 159 annonces actives sur Lou-Ka · site web ↗ ★ 3,6 395 avis Google — fiche « Logisco - Siège social » 5★ 1 4★ 0 3★ 0 2★ 0 1★ 7 Moyenne des 12 derniers mois : 1,5 (8 avis) Plaintes récurrentes dans les avis : SERVICE CLIE","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3721bd6f-6f25-40b4-9815-0235fa0c0812","timestamp":"2026-08-24T22:17:12.059Z","tool_use_result":{"stdout":".f-gest OK | Qui gère ce logement? Logisco 159 annonces actives sur Lou-Ka · site web ↗ ★ 3,6 395 avis Google — fiche « Logisco - Siège social » 5★ 1 4★ 0 3★ 0 2★ 0 1★ 7 Moyenne des 12 derniers mois : 1,5 (8 avis) Plaintes récurrentes dans les avis : SERVICE CLIE","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM7bsxb9nR86fM3tCVw","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIShwMKpwEIERgCKkBhZRwSVknILvKS2o3vCZqMR6my80Cb/HXAwHxUUs8d5/YL3CZzWTACfueg4QxA9LTsiiLWTuLQYjfQok3BY45yMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHshbPUBrABAhIMjuWd9xsKprYpZ6I8GgzOneRm6LhwsY7MAOIiMDrGLzB2A+Yxfto83ODBFl2JlVlo/0HCjF2Dw+n8kzomXjkSMw9ZkOeS1ORGat7+JCqMAYfNDqlF2TL07yqyZ/nlirR93tXAFqzRVlv3/it9Mg04dY5vjL+Qh20/Qkd4uzq8I351KTYG/vnG+djVUZUfCZa65WEyPpxwNJL8eDwWLVBmrckHPzCQSTV0gveebeptnH+nUwME+NaGBHK2H4UGdT/3kT20gxGOxSBZFeW51sId8M6AHj7gFBWbZNHJGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1415,"cache_read_input_tokens":62777,"cache_creation":{"ephemeral_5m_input_tokens":1415,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f1a55fe0-d678-4cf5-9dbe-7a5323d3150a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM7bsxb9nR86fM3tCVw","type":"message","role":"assistant","content":[{"type":"text","text":"Les cinq blocs s'affichent en production. Reste le commit — je vérifie d'abord que la clé SerpApi n'apparaît nulle part dans le diff et que `.env` reste hors suivi."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1415,"cache_read_input_tokens":62777,"cache_creation":{"ephemeral_5m_input_tokens":1415,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2b93176a-1762-4865-9fd7-e932f899e7e8"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM7bsxb9nR86fM3tCVw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Qi6FCkcQfNPq6FPJZ61YaE","name":"Bash","input":{"command":"git status --porcelain; echo \"---\"; git check-ignore .env && echo \".env ignoré\"; echo \"---\"; git diff HEAD --stat | tail -5","description":"Git status, .env ignore check, diff stat"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1415,"cache_read_input_tokens":62777,"cache_creation":{"ephemeral_5m_input_tokens":1415,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fe2ee84f-c91d-46a6-abc8-ada0327d8d3e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Qi6FCkcQfNPq6FPJZ61YaE","type":"tool_result","content":" M frontend/src/styles.css\n M louka/connectors/kangalou.py\n---\n.env\n.env ignoré\n---\n frontend/src/styles.css | 166 +++++++++++++++++++++++++++++++++++++++++++\n louka/connectors/kangalou.py | 18 ++++-\n 2 files changed, 181 insertions(+), 3 deletions(-)","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9e34dd29-da82-4241-9473-bd181c1b5dcd","timestamp":"2026-08-24T22:17:18.291Z","tool_use_result":{"stdout":" M frontend/src/styles.css\n M louka/connectors/kangalou.py\n---\n.env\n.env ignoré\n---\n frontend/src/styles.css | 166 +++++++++++++++++++++++++++++++++++++++++++\n louka/connectors/kangalou.py | 18 ++++-\n 2 files changed, 181 insertions(+), 3 deletions(-)","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM7bsxb9nR86fM3tCVw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TFcVGLhVynSsMNtgqH27sx","name":"Bash","input":{"command":"git diff HEAD | grep -c cede959; git diff HEAD | grep -ci \"serpapi_api_key\\s*=\" ; grep -rn \"cede959\" --include=\"*.py\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.md\" --include=\"*.example\" . 2>/dev/null | head -3; echo \"scan terminé\"","description":"Scan diff and tracked files for the SerpApi key"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1415,"cache_read_input_tokens":62777,"cache_creation":{"ephemeral_5m_input_tokens":1415,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7c6b4ed9-24b7-4873-b580-40cb597d19ed"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TFcVGLhVynSsMNtgqH27sx","type":"tool_result","content":"0\n0\nscan terminé","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"51784d2a-e051-4373-888e-a94f0a01e38c","timestamp":"2026-08-24T22:17:19.825Z","tool_use_result":{"stdout":"0\n0\nscan terminé","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"7c3fed58-0433-4fc6-b1b5-e864f77c077e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM8B21Y5qJBKKwV1aBN","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIStgYKpwEIERgCKkAd0KtMyC28uLBm/3Z8o/pJd2zotLvJp8v9q7tv/CLFQxAlqxdZPBvZN73Q0BnG1u+uA+1FLMVhssJ+GfVvKlAkMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH2hbPUBrABAhIM2gHB6VxC2iO8bFk5GgwFy79ZBHIWUI5SoD4iMEkpuU8PU+EGW4bTm0ban+V5xmk0ooAfE1KisDAKF2dJ2AyLfWhH2n1J0mXe8OcqSiq7BIik21ho5eB6HIzOIrakwxB61aSNtoaeu/QThJ8OOGsQCSE+cwr0SDSm1QJ/IQn1BAnS4o7T9UKFFvNZPndJY5wSrzBdTGRi+fLhQh/ucQUzmcRJNygTn9hnc2C7VEYHPW8f/nhsOxahmq6RfUc5N5TJBmqsQtMFYoYWXu5RzGt95Ipu64NGmEqdZ7uT7wAXYTpzzSoIRDKHNE1s2ySB38kYauNz41W54oQaM76w6P3Y/p2U7N2UUggvYyMaL6qxaBwfO4yBWvdSKKRTiIFlqq0b9KJQGcpKdo59CtFqVXovf1k8vxYnJkFUUr2qqMo/YQsw3rijd0WgiCUx6pVTEIJb/sUuhwv7XoblO+AAdeud6Wr76brJHGyXHpBwK/48ECq4mPiqKrX3jwunsWkO+nQCOdLriIVPThVQcfnS9pF+pQWrH1gZZtetc2WxollOTIQGbMvaQO4MeJwqcB+lpshc1aJ/scJTWL+oZ0fw+0D+mD7YUz51XtXHlI1uXJ25eBa06xpIY1tp8qwEbL0IolN9gBDW/iVCEnXn/GsDT62tsJHhOjQ0+CkI5Cydi80tLDH8lQSi2O5M2S0UJCyObBkCKyY79S+GJX6WR1yiC2obSfxElIjYTmfqiKRO4GvE6stFREILei+2dcumpuGhMt0KOLoTgp3emuoMvq8/HzZ2NQDyNDD9W/ynprtPJlI/deFKGz/0DR61hzidvgrH/yblojGPLy5il0aTCosvtBLZyQnSIeeKJT0L+pgYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":644,"cache_read_input_tokens":64192,"cache_creation":{"ephemeral_5m_input_tokens":644,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e20c9918-0374-41f7-b515-8befde916c31"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM8B21Y5qJBKKwV1aBN","type":"message","role":"assistant","content":[{"type":"text","text":"Surprise : seuls styles.css et kangalou.py sont modifiés — le reste a peut-être déjà été commité. Je vérifie l'historique et cette modification de kangalou.py que je n'ai pas faite."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":644,"cache_read_input_tokens":64192,"cache_creation":{"ephemeral_5m_input_tokens":644,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"814350a9-93d3-471a-8a06-de7440df1a60"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM8B21Y5qJBKKwV1aBN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CjVgQaYUvZobgjnrNQTBW4","name":"Bash","input":{"command":"git log --oneline -8; echo \"---\"; git log --oneline -3 -- louka/managers.py frontend/src/components/CoutReel.tsx","description":"Check if dossier work already committed"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":644,"cache_read_input_tokens":64192,"cache_creation":{"ephemeral_5m_input_tokens":644,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0da58207-24cb-4ee6-be15-102b150598bd"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CjVgQaYUvZobgjnrNQTBW4","type":"tool_result","content":"0ee1a2d [ka2] snapshot pré-mission kangalou\n8efd8cb [ka2] snapshot pré-mission immobilia\n6ff906e feat(pdf): fiche PDF complète — analyse de prix, registre, TAL, environnement, KA Scores\n872a202 feat(design): passe « éditorial sharp » sur la fiche — cartes, KPI, tableaux, pastilles\n6a6acdf feat(tal): historique du Tribunal administratif du logement par adresse\n438b294 docs: README v3 — chiffres live vérifiés, pastilles dynamiques, sections complètes\n5b2bff8 [ka2] fix connecteur beaudoin: échouer franchement sur erreur transitoire (page 1 / taxonomie ville) au lieu d'un faux succès à 0 annonce\n4fa8714 docs: README v2 — galerie multi-pages, style du site, documentation, contact\n---","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d223744b-e570-4538-b424-365ff7b72e3c","timestamp":"2026-08-24T22:17:27.555Z","tool_use_result":{"stdout":"0ee1a2d [ka2] snapshot pré-mission kangalou\n8efd8cb [ka2] snapshot pré-mission immobilia\n6ff906e feat(pdf): fiche PDF complète — analyse de prix, registre, TAL, environnement, KA Scores\n872a202 feat(design): passe « éditorial sharp » sur la fiche — cartes, KPI, tableaux, pastilles\n6a6acdf feat(tal): historique du Tribunal administratif du logement par adresse\n438b294 docs: README v3 — chiffres live vérifiés, pastilles dynamiques, sections complètes\n5b2bff8 [ka2] fix connecteur beaudoin: échouer franchement sur erreur transitoire (page 1 / taxonomie ville) au lieu d'un faux succès à 0 annonce\n4fa8714 docs: README v2 — galerie multi-pages, style du site, documentation, contact\n---","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM8B21Y5qJBKKwV1aBN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01T45d8KnoW2LsxRemrFgYBM","name":"Bash","input":{"command":"git diff HEAD -- louka/connectors/kangalou.py","description":"Show kangalou.py diff"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":644,"cache_read_input_tokens":64192,"cache_creation":{"ephemeral_5m_input_tokens":644,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a59cba94-72ad-45a4-b80e-928b94058ce6"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01T45d8KnoW2LsxRemrFgYBM","type":"tool_result","content":"(Bash completed with no output)","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"71160077-8a63-473b-89f3-5594e44412a6","timestamp":"2026-08-24T22:17:28.041Z","tool_use_result":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"094cd4c5-076b-41d4-8a19-fc2f621c0faa","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM8n2RnzEPNyaM3X5ej","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":875,"cache_read_input_tokens":64836,"cache_creation":{"ephemeral_5m_input_tokens":875,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f961db81-05bf-4354-a836-6ac6e1825a50"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM8n2RnzEPNyaM3X5ej","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JJcGdXHF4kRSxGLyDPgjt4","name":"Bash","input":{"command":"git status --porcelain; echo \"---\"; ls -la louka/managers.py louka/serpapi_client.py frontend/src/components/CoutReel.tsx 2>&1; echo \"---\"; git ls-files louka/managers.py frontend/src/components/CoutReel.tsx; echo \"---\"; git show --stat 0ee1a2d | head -20","description":"Re-check status, file existence, tracked state"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":875,"cache_read_input_tokens":64836,"cache_creation":{"ephemeral_5m_input_tokens":875,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fc675298-e924-456b-a0a2-4a1cfdf99075"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JJcGdXHF4kRSxGLyDPgjt4","type":"tool_result","content":" M frontend/src/styles.css\n M louka/connectors/kangalou.py\n---\nls: frontend/src/components/CoutReel.tsx: No such file or directory\nls: louka/managers.py: No such file or directory\nls: louka/serpapi_client.py: No such file or directory\n---\n---\ncommit 0ee1a2d01bff976a8d7a5d31b78477c404ec7226\nAuthor: Simon-Pierre Boucher \nDate: Mon Aug 24 18:14:15 2026 -0400\n\n [ka2] snapshot pré-mission kangalou\n\n frontend/src/api.ts | 146 +++++++++++++++++++++++++++\n frontend/src/components/CoutReel.tsx | 98 ++++++++++++++++++\n frontend/src/components/GestionnaireBloc.tsx | 129 +++++++++++++++++++++++\n frontend/src/components/HistoriqueLouka.tsx | 126 +++++++++++++++++++++++\n frontend/src/components/HiverScore.tsx | 43 ++++++++\n frontend/src/components/ImmeubleBloc.tsx | 113 +++++++++++++++++++++\n frontend/src/pages/Listing.tsx | 15 +++\n 7 files changed, 670 insertions(+)","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"48ce255f-d381-4b1b-8c95-7a5fec8887dd","timestamp":"2026-08-24T22:17:39.842Z","tool_use_result":{"stdout":" M frontend/src/styles.css\n M louka/connectors/kangalou.py\n---\nls: frontend/src/components/CoutReel.tsx: No such file or directory\nls: louka/managers.py: No such file or directory\nls: louka/serpapi_client.py: No such file or directory\n---\n---\ncommit 0ee1a2d01bff976a8d7a5d31b78477c404ec7226\nAuthor: Simon-Pierre Boucher \nDate: Mon Aug 24 18:14:15 2026 -0400\n\n [ka2] snapshot pré-mission kangalou\n\n frontend/src/api.ts | 146 +++++++++++++++++++++++++++\n frontend/src/components/CoutReel.tsx | 98 ++++++++++++++++++\n frontend/src/components/GestionnaireBloc.tsx | 129 +++++++++++++++++++++++\n frontend/src/components/HistoriqueLouka.tsx | 126 +++++++++++++++++++++++\n frontend/src/components/HiverScore.tsx | 43 ++++++++\n frontend/src/components/ImmeubleBloc.tsx | 113 +++++++++++++++++++++\n frontend/src/pages/Listing.tsx | 15 +++\n 7 files changed, 670 insertions(+)","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"bff969e5-7a18-4cc7-aba9-f877886ff336","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM9eaEbQRxh5EeKZda5","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":65711,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"14d647be-b62d-4ca6-ae83-4f02cc489297"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM9eaEbQRxh5EeKZda5","type":"message","role":"assistant","content":[{"type":"text","text":"Alerte : une session parallèle « ka2 » committe des snapshots dans le même repo et `louka/managers.py`, `serpapi_client.py` ont disparu du disque. J'investigue avant toute chose."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":65711,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"360c45af-8cb5-4b0a-90a1-142fd4d9e711"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM9eaEbQRxh5EeKZda5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VPeE6ZmhJQPLGiQBAS8A9K","name":"Bash","input":{"command":"git log --oneline -5; echo \"---reflog---\"; git reflog -15; echo \"---stash---\"; git stash list","description":"Check HEAD, reflog, stashes"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":65711,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6074d77e-5732-4b1d-b0cc-270a2d6ad8fc"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VPeE6ZmhJQPLGiQBAS8A9K","type":"tool_result","content":"0ee1a2d [ka2] snapshot pré-mission kangalou\n8efd8cb [ka2] snapshot pré-mission immobilia\n6ff906e feat(pdf): fiche PDF complète — analyse de prix, registre, TAL, environnement, KA Scores\n872a202 feat(design): passe « éditorial sharp » sur la fiche — cartes, KPI, tableaux, pastilles\n6a6acdf feat(tal): historique du Tribunal administratif du logement par adresse\n---reflog---\n0ee1a2d HEAD@{0}: commit: [ka2] snapshot pré-mission kangalou\n8efd8cb HEAD@{1}: commit: [ka2] snapshot pré-mission immobilia\n6ff906e HEAD@{2}: commit: feat(pdf): fiche PDF complète — analyse de prix, registre, TAL, environnement, KA Scores\n872a202 HEAD@{3}: commit: feat(design): passe « éditorial sharp » sur la fiche — cartes, KPI, tableaux, pastilles\n6a6acdf HEAD@{4}: commit: feat(tal): historique du Tribunal administratif du logement par adresse\n438b294 HEAD@{5}: commit: docs: README v3 — chiffres live vérifiés, pastilles dynamiques, sections complètes\n5b2bff8 HEAD@{6}: commit: [ka2] fix connecteur beaudoin: échouer franchement sur erreur transitoire (page 1 / taxonomie ville) au lieu d'un faux succès à 0 annonce\n4fa8714 HEAD@{7}: commit: docs: README v2 — galerie multi-pages, style du site, documentation, contact\n963ba8f HEAD@{8}: commit: feat(doc): page documentation /doc (guide + captures) + lien footer + PDF téléchargeable\nc4c6950 HEAD@{9}: commit: docs: refonte du README (pastilles + captures d écran à jour)\n2dc6833 HEAD@{10}: commit: stats : filtre annonces actives adaptatif au schéma (_active_where) dans les panneaux population\n2d725b8 HEAD@{11}: commit: Stats : panneaux territoire (registre des loyers, population, essence)\n4d17c17 HEAD@{12}: commit: hydro : postal rooftop (géocodage avant Mapbox + proximity), cache négatif, fix civiques ordinaux\n45c7645 HEAD@{13}: commit: hydro : cache par uid + dérivation du code postal via lat/lng (Mapbox)\n64876d4 HEAD@{14}: commit: hydro : pré-calcul des 100 annonces récentes + auto pour les nouvelles\n---stash---","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1fa7b102-5858-4684-9542-fc37ac375568","timestamp":"2026-08-24T22:17:58.021Z","tool_use_result":{"stdout":"0ee1a2d [ka2] snapshot pré-mission kangalou\n8efd8cb [ka2] snapshot pré-mission immobilia\n6ff906e feat(pdf): fiche PDF complète — analyse de prix, registre, TAL, environnement, KA Scores\n872a202 feat(design): passe « éditorial sharp » sur la fiche — cartes, KPI, tableaux, pastilles\n6a6acdf feat(tal): historique du Tribunal administratif du logement par adresse\n---reflog---\n0ee1a2d HEAD@{0}: commit: [ka2] snapshot pré-mission kangalou\n8efd8cb HEAD@{1}: commit: [ka2] snapshot pré-mission immobilia\n6ff906e HEAD@{2}: commit: feat(pdf): fiche PDF complète — analyse de prix, registre, TAL, environnement, KA Scores\n872a202 HEAD@{3}: commit: feat(design): passe « éditorial sharp » sur la fiche — cartes, KPI, tableaux, pastilles\n6a6acdf HEAD@{4}: commit: feat(tal): historique du Tribunal administratif du logement par adresse\n438b294 HEAD@{5}: commit: docs: README v3 — chiffres live vérifiés, pastilles dynamiques, sections complètes\n5b2bff8 HEAD@{6}: commit: [ka2] fix connecteur beaudoin: échouer franchement sur erreur transitoire (page 1 / taxonomie ville) au lieu d'un faux succès à 0 annonce\n4fa8714 HEAD@{7}: commit: docs: README v2 — galerie multi-pages, style du site, documentation, contact\n963ba8f HEAD@{8}: commit: feat(doc): page documentation /doc (guide + captures) + lien footer + PDF téléchargeable\nc4c6950 HEAD@{9}: commit: docs: refonte du README (pastilles + captures d écran à jour)\n2dc6833 HEAD@{10}: commit: stats : filtre annonces actives adaptatif au schéma (_active_where) dans les panneaux population\n2d725b8 HEAD@{11}: commit: Stats : panneaux territoire (registre des loyers, population, essence)\n4d17c17 HEAD@{12}: commit: hydro : postal rooftop (géocodage avant Mapbox + proximity), cache négatif, fix civiques ordinaux\n45c7645 HEAD@{13}: commit: hydro : cache par uid + dérivation du code postal via lat/lng (Mapbox)\n64876d4 HEAD@{14}: commit: hydro : pré-calcul des 100 annonces récentes + auto pour les nouvelles\n---stash---","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNM9eaEbQRxh5EeKZda5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NgqgCzNUsYvU3NYZPkqwcA","name":"Bash","input":{"command":"grep -n \"cout-reel\\|managers.fiche\\|immeuble\" louka/web.py | head; echo \"---\"; grep -n \"precompute\\|building.rollup\" louka/ingest.py run.py | head; echo \"---\"; git log --all --oneline -- louka/managers.py louka/serpapi_client.py louka/building.py louka/history.py louka/coutreel.py louka/hiver.py louka/recycled.py","description":"Check backend changes still present"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1120,"cache_read_input_tokens":65711,"cache_creation":{"ephemeral_5m_input_tokens":1120,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"776fcb7b-586a-4d0c-8ad7-4ec8d6b32643"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NgqgCzNUsYvU3NYZPkqwcA","type":"tool_result","content":"grep: louka/web.py: No such file or directory\n---\ngrep: louka/ingest.py: No such file or directory\ngrep: run.py: No such file or directory\n---","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f02c0496-0dbb-4d95-b410-513b5ef3536e","timestamp":"2026-08-24T22:17:59.895Z","tool_use_result":{"stdout":"grep: louka/web.py: No such file or directory\n---\ngrep: louka/ingest.py: No such file or directory\ngrep: run.py: No such file or directory\n---","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"b731830b-8048-41ed-9f8b-4993c00a48cb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMB95WkwzqxHaqYxrgo","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISvQsKpwEIERgCKkD1iRSh+I1JYB1KSdnM0gXYt5Zbss5d5amhxCfzgn0hZOkxzjTWQJUa/lPbqE3fuUW2MkX2Ecb4W0dF0LQgtBVyMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGjhrPUBrABAhIMCecTPZUS0UD2b/P5Ggz3l48ohGsgRDdrBTsiMB64E4Ix+z3VxQnYavgaKlgculiI6ckwvEqpS+VsD+VN3rTR7TzZ2vc7gEjHBmUoOirCCeQKbbsuV4xkJS2js6FIArPD2Y4Iq6/KgoUlPaeFLhynZnxga57t2nVlRiAGSMMbOiWWnYhHBN9JZtIq6c90BZMEtffp6pbsN3mv5MFNT2HAJKOlALl6ZC12G9vWTLvO5P98BuF14iV8C8mL5huOWXK80gb7KX1X+8ndlaN1kdyI3lIbNXz0VPoN74lFkQKwZccRUiljehcgF70FSK36tg4Nm7jFV7VK3A9VgpS2R7Daz/n3G8c/1ayy/lxBHyxtL+wJyckXlaEz2dDzHhIaNclHTR7eztFxEiweguEjZ4bRKxc/F2xa8WMLYC7Y28X/4PYw0+AI8NpWzUl/7XDzd+mtdqwGliffHaGvdMLDenysL5XhR1iL6vNDxqcofD/HhixRV4mJwUmJuhC2Ko3Z12aw7OEQiXFEpBCe4AqRk8k9QzgSQlCqeD1dpRUgZErh5l5MrDV0ZmCVAm7yeYwSSy8qiWv70VjiHeE6ipPZmmMHko2mVdh4SCpYt4YoUBhXReocvt+U6RDbLYqltFia32VZAu+L+FgI+50F4nRTApbkzFy7T1scSundIc2mTEEmHiq/nq1vc+XBu24pv6xZj4sEFbOp55NWsov11HV/0uUw5bes0BLWIZeO3JB/xrto0H/kZfOyUpqhoBjfdAyjnChkaDX0PgXkwNYyr48O6hk++gU4uOHbUGj4m5cKAu8tnFnwDKvrreGVKIQetOdXe7TZwHwIa1MClZwMPQEs/2zUZQimy/6+LheT+XWh4ns6tBVnL66mLoC9Ve2cI1EJnN99EBV5lkPQyZJ96TkzOJ5kCJecRGpOI4z/KKhAHDwxIbuT8tdH3DJLrYp7pYxIhpl88MNQ562WvD7SMmtbbNTphVXWjpulW2srqRalEam8DFhQfLj4AITsfCb2OtJz5OeTk8ZyX1XxO8d7PU5RN6HX8Y7/qbgk5iQCCpNDgIfx3dyKF9a5wcfqsutODWNBjud60VXGZ7ziHlzHX+fJyqlkmbFw7AGETrCDz1NM0wheMlggoIQLIIJpFiKbqhJptV+nwMZKfdgFBB4aHqDzVSWJ+BAG3euD94Honh79xs9S9UfX72LSmh+FnKds8e9InEZqRaLMzvcOQWHmsnhSlP5wGu5KNbT+315/3kOwaJ9tgxYncZW45WM+qmUOAo/fh5wdf8BBl/XturlnNuH/rkRpQ+6x6QdvxQDsE7922pO2iJ7MpOIrX0DXPAW2Theoaj3nN1UnjG/yBkng8UXsQ1yPSk2rWtcolFsen0Z0WbPGzjQTIoP2VKucKsgNZuc9bUDtbLFD2Ob9OZIT10soae0+zaZN8PYReHs2+nH/hT4yX3JJU3evJsuYrxp7lXUd+9Ol4p3mnk3McgsxNIIUZl2/cuyoA+5mueA9aU64unF459ldPlHl04LNHm6tVjguLVEIm4Qep0v4vs1TERHSMwBkFW1so5ANfsf2GiseoCHWPV/mXz5kJ6trBo2Hg2cFanhaFNOxwaDRbLg1qR1uMBCZGN65ZmXbgaKUH5SBobq7fnaSST1y8DBVYOy7ObfgpXh53lZSra3x6eZwQ9ZABPOIxiOJjpbgVEE66Ogz3ohirxRjgDWywYl1LIN804K55aqpnxgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2955,"cache_read_input_tokens":66831,"cache_creation":{"ephemeral_5m_input_tokens":2955,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"41a4c147-153b-45e1-9cad-ebd651d5b961"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMB95WkwzqxHaqYxrgo","type":"message","role":"assistant","content":[{"type":"text","text":"Fausse alerte probable : mon répertoire courant est resté dans `frontend/`. Je reviens à la racine et je revérifie."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2955,"cache_read_input_tokens":66831,"cache_creation":{"ephemeral_5m_input_tokens":2955,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9be4fabc-c0e7-495a-af5b-5f0cc6919f57"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMB95WkwzqxHaqYxrgo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BDjgbDN3Xv4uDj8aR6mUA1","name":"Bash","input":{"command":"cd /Users/simon-pierreboucher/apps/lou-ka && pwd && ls louka/managers.py louka/serpapi_client.py louka/web.py run.py frontend/src/components/CoutReel.tsx && git show --stat 8efd8cb | head -30","description":"Verify files exist from repo root and inspect immobilia snapshot"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2955,"cache_read_input_tokens":66831,"cache_creation":{"ephemeral_5m_input_tokens":2955,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"da424fe4-b1c7-4cdd-ae39-459caef42453"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BDjgbDN3Xv4uDj8aR6mUA1","type":"tool_result","content":"/Users/simon-pierreboucher/apps/lou-ka\nfrontend/src/components/CoutReel.tsx\nlouka/managers.py\nlouka/serpapi_client.py\nlouka/web.py\nrun.py\ncommit 8efd8cb62c2b7e4c21bf9d61c4a223381ec7d58a\nAuthor: Simon-Pierre Boucher \nDate: Mon Aug 24 18:09:13 2026 -0400\n\n [ka2] snapshot pré-mission immobilia\n\n .env.example | 29 +++\n louka/building.py | 244 ++++++++++++++++++++++\n louka/coutreel.py | 196 ++++++++++++++++++\n louka/db.py | 141 ++++++++++++-\n louka/history.py | 95 +++++++++\n louka/hiver.py | 108 ++++++++++\n louka/ingest.py | 14 ++\n louka/managers.py | 529 ++++++++++++++++++++++++++++++++++++++++++++++++\n louka/recycled.py | 185 +++++++++++++++++\n louka/serpapi_client.py | 86 ++++++++\n louka/web.py | 59 ++++++\n run.py | 12 ++\n 12 files changed, 1697 insertions(+), 1 deletion(-)","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"846d5056-f476-4661-bdc3-33da3c7707e1","timestamp":"2026-08-24T22:18:12.181Z","tool_use_result":{"stdout":"/Users/simon-pierreboucher/apps/lou-ka\nfrontend/src/components/CoutReel.tsx\nlouka/managers.py\nlouka/serpapi_client.py\nlouka/web.py\nrun.py\ncommit 8efd8cb62c2b7e4c21bf9d61c4a223381ec7d58a\nAuthor: Simon-Pierre Boucher \nDate: Mon Aug 24 18:09:13 2026 -0400\n\n [ka2] snapshot pré-mission immobilia\n\n .env.example | 29 +++\n louka/building.py | 244 ++++++++++++++++++++++\n louka/coutreel.py | 196 ++++++++++++++++++\n louka/db.py | 141 ++++++++++++-\n louka/history.py | 95 +++++++++\n louka/hiver.py | 108 ++++++++++\n louka/ingest.py | 14 ++\n louka/managers.py | 529 ++++++++++++++++++++++++++++++++++++++++++++++++\n louka/recycled.py | 185 +++++++++++++++++\n louka/serpapi_client.py | 86 ++++++++\n louka/web.py | 59 ++++++\n run.py | 12 ++\n 12 files changed, 1697 insertions(+), 1 deletion(-)","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"3ccd876b-7600-4075-a632-f8639804f5eb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMC3CaNmUc1kf6fWi3x","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISkAcKpwEIERgCKkBWOhT5ATuYNIUM7uqfH1MhGrh4MVGl1+H+RWVPtmT2Vq7yCvSh3aIog0HOZEsLvKipzBNopWXV++hZ5xJ1Cx9iMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGuhrPUBrABAhIMYggTM+CMDn0AqkeKGgwpJTl867eBgbOAIH4iMJS1qTInJujnluWrlbq9f+qntBdD5DFsk88tyrxrG5yXrZs1QxxxtBYagpFSgpdK8CqVBW7DDfe1PV3A3H3baB3arb/nYNvJ4QL0R93g7PFLS+B4uDEdS1Rey9j9sQNfB2n1JwAn0qJjurT6IN33kc0wK47nVAw6RSZ+QIF/5nwpvJr92YRkaRObSw03kaRG0qGSbMnyBNJFLoG27Qm6re0wr/pn1CoevNBy0/SOON69afuo3wSrEW/XbQANk9GQVjTM5BG+QLtH1lFPgdw6uFbXftrLUikPzNcbMDtca245EBPnQEwV60AKRcAywKJmXNVdIMd8kzV7uNpwZhjdXLYqSy9SQfSAaTS6KI/clp72o8TnwqoVPDF0DZz7laHgTPeQeKgMyPGgc+AyiYMc+izBAYTgzGzPovCQUzDkju7gNYxtyoZ8cgIGQoBovt3Dq1QqQO2P81WqqIN8dCdxv0JuR0xGe22RuQhL26bvyilz0+I/yyGm+xa/GAWxcaIEx6mGqG0kRZmUN4me4MJthtHvXR40Xe4SxIOzyHcYcVKz6bCY/HMnrA5HnvVSr+wH8UYctCBCdYAaf981Zol95drMFZBeyTms2UBXdZvQEINP/SMFcW7THawKusL6E7rNbBQgczaAsGHrGM6inCjVRwwHPldm3LQNesc4xHeG8NOAJ0NUmDsNbF/nB6O31ZkveV6U1gv+LUgqadGyqfoE3aruYcb7XKgVWiU7bi1gOhXteuerbn21dFxO20OAfBxH6v8k3n3QEJ/0AIMqbmqzWPfXnNWVH27EquqPmjrtUd+MeSavPW2jzqwNBc3GiilBu8cyK6khyxIXeKMJ5f6Ngo4pgA/9H+n3Cv5mLjqLnybiIzujlTJJ2q+hnvDHZhbgI4yakYrAK+fWTIfIb1ZDj8BGr+u84H11r7iaMHbJlReiBYElYptjlfcYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1160,"cache_read_input_tokens":69786,"cache_creation":{"ephemeral_5m_input_tokens":1160,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e09bab08-c7ad-4163-aa52-ca8ad2c57d2f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMC3CaNmUc1kf6fWi3x","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISrAQKqAEIERgCKkD1SM4HnOS10LY4jrozRF92K6ojkqF+l4pMqKY3RXRzSWsqkt+uf/Kwi42C6YUgA9gAFPh7iwjXLSm/D02zYFA6Mg5jbGF1ZGUtZmFibGUtNTgBQgluYXJyYXRpb25aJDZmY2M4ZjgzLTFmOWMtNGQzMy1iYTA3LTRkNGRmMzdkNjdmNXIQi06A1NuRRJo41amXnK2lOYgBAagBroaz1AawAQISDB5XO0KMMb69/ZC5QRoMRSxZve0fvUOp49ShIjBi8Ekcp8/iEIkGVRpvtmqftz7Mmhz3zGf6AW/VHa1v7J5WNPZ6vjlEyaypZkEZNn8qsAJRGKWzgmZGx21M3C8qG5RxRuTSy5L9P6tmC/+LBqcPcP0Bzsq1BofLaoGHaG4PpMtLH4Mb4KvLrn1fnYW6sS61CUd7LMQKeAKz7JrF915vID9q7VWETYdGyQIAKm80af6yALNjVDOBbV/ORBBsay0wBucVSMNRzSnKIHZGTNdIlwIohXognAWppKlTaHHOXpLhcHDKJO1DSVLfXfew4e66UZt+WXxx7IAPCkRHM4pHid3gQ2aw5JlTvnzw2unUB9a+pAqPhG4sRr7kCMh4ST6O0mq8kKw1UyV33PWh7qVQca+S5N2kXwjxKU3gJPGMb14JOOjm2lAlTMXIrbTqONdweb4kc4xKVJWVzFOpsyPm1g6E3uXDPLUDYCmvGv6JyxQ3C83OvFxUlmLeRh1vplXRGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1160,"cache_read_input_tokens":69786,"cache_creation":{"ephemeral_5m_input_tokens":1160,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4bbbc15c-da88-4414-8df7-d203fc3756b7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMC3CaNmUc1kf6fWi3x","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01F9qLDqUzecQCcN68EpRN2h","name":"Bash","input":{"command":"git show 8efd8cb:.env.example | grep -i serpapi; echo \"---\"; git log -p 8efd8cb 0ee1a2d --format=oneline -2 | grep -c cede959; echo \"clé absente des snapshots si 0 ci-dessus\"","description":"Verify SerpApi key absent from snapshot commits"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1160,"cache_read_input_tokens":69786,"cache_creation":{"ephemeral_5m_input_tokens":1160,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3d344b13-7b88-41cf-9f33-64c57d7cf994"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01F9qLDqUzecQCcN68EpRN2h","type":"tool_result","content":"# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\nSERPAPI_API_KEY=\n---\n0\nclé absente des snapshots si 0 ci-dessus","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d5e85567-f4eb-4eee-8bae-65aaa631f698","timestamp":"2026-08-24T22:18:24.074Z","tool_use_result":{"stdout":"# SerpApi — résolution Google Maps + avis des gestionnaires (louka/managers.py)\nSERPAPI_API_KEY=\n---\n0\nclé absente des snapshots si 0 ci-dessus","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"a70bd802-10dd-4dec-877f-2b2699ffc4f1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMCuhA1AHY5pAiH6q1E","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISwAMKpwEIERgCKkCEcTlNMghxkT7IBtWtUE2X4ur/GLDnXEpsN6abuFIA0qHST/JQdl+TZdLz/ZYHO+zQRmnnEfdwcOznIe4AkmUvMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG1hrPUBrABAhIMxNJY50N9yzeYhhOwGgzveuNyqe+G7PjGE30iMHUh8wVHpTcJ7cilgN5rmpDvahAK82kO7jTQ5kUXmafxLJV/3mKsTHqTAQ+zZOX3/CrFAYy0BKhNOaIpnwQ80Yzgfi+ICtczPu7i71GCKdHS+zn+kscpfcyqTRKi5iEzjitVuBcD2/sZXIjSaP6wk3tcn2hAX0T9GfzFQ4pqyPu6FBDSrn0nXe3njUhVjVwpXrb4RJSvP9GA3UZ7Ktc8mHydFCO8MkXrzn5GDd8/tR5OOPndtQhw9qOZ1fUIBpPaFvZk5CbQmL1IK6h6hrlkbnrSeRjGlnlLm1bNpFpLkrVcYiyyueMPY6B69eEp3+WUIDh150WQRHStGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":634,"cache_read_input_tokens":70946,"cache_creation":{"ephemeral_5m_input_tokens":634,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"40aa8322-6000-4401-9e65-c45c51213c60"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMCuhA1AHY5pAiH6q1E","type":"message","role":"assistant","content":[{"type":"text","text":"Aucune fuite : `.env.example` a une valeur vide et la clé n'apparaît dans aucun commit. Je committe styles.css seul et je pousse."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":634,"cache_read_input_tokens":70946,"cache_creation":{"ephemeral_5m_input_tokens":634,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f832944b-8ef9-4df8-bb59-529643f5a40e"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMCuhA1AHY5pAiH6q1E","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KrvJjvByQjMsVHDA816WEC","name":"Bash","input":{"command":"git add frontend/src/styles.css && git commit -m \"$(cat <<'EOF'\nfeat(fiche): styles du dossier d'intelligence locative — coût réel, historique, immeuble, gestionnaire, hiver\n\nComplète les snapshots ka2 (backend 8efd8cb + composants 0ee1a2d) : pastilles\nde statut de donnée (observé/estimé/inconnu), tableau du coût réel, timeline\nLou-Ka, distribution des avis Google, score hiver.\n\nCo-Authored-By: Claude Opus 4.7 \nEOF\n)\" && git push origin main && git status --porcelain","description":"Commit styles.css and push to spbgit"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":634,"cache_read_input_tokens":70946,"cache_creation":{"ephemeral_5m_input_tokens":634,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c76a51e5-1c39-4fe2-a8ff-2582e5b807f7"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KrvJjvByQjMsVHDA816WEC","type":"tool_result","content":"[main 521837e] feat(fiche): styles du dossier d'intelligence locative — coût réel, historique, immeuble, gestionnaire, hiver\n 1 file changed, 166 insertions(+)\nTo gitsrv:srv/git/lou-ka.git\n 0ee1a2d..521837e main -> main\n M louka/connectors/kangalou.py","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"93974c9d-7d4b-4ce9-b3ce-828647e72ff9","timestamp":"2026-08-24T22:18:32.612Z","tool_use_result":{"stdout":"[main 521837e] feat(fiche): styles du dossier d'intelligence locative — coût réel, historique, immeuble, gestionnaire, hiver\n 1 file changed, 166 insertions(+)\nTo gitsrv:srv/git/lou-ka.git\n 0ee1a2d..521837e main -> main\n M louka/connectors/kangalou.py","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"00228de0-3cc3-4fda-a04f-38be353bbfdd","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMDYmMNLCxt2uX8Cwze","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIStQcKpwEIERgCKkAJJYYcFZPmW8dBjzWA6GbD7SsQCO4kad1ONMCGo67QqSrCwsdNNIHj5MEKHntCLA77qvIdun2vvGb38372UOa5Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHGhrPUBrABAhIMGSzMz+Mr3JP4ojTyGgxP4XaSc3kEAXBSc7QiMN5ztM71uKJv2GxV5XLBpAVcKKZND3UNUMnJgw5Y9heedfM10vx6kV43u7AmgC+kMyq6BfuPpSF+Cc8Hfbdmo7el52wyb0QLQaqK6i12Me6Z9wJfdx7Y55VGpcagMdP1rXC9UVBHNG744XsEsM2D/6Hy6xY+6m3e7N0eQCpE0qsj6nkixWjMkAf4Wri6Ntld+WbTuQvvPoQxNKKWYLeVg0e78qK19UP/DtfL4PGJhGqzh/rkAapZBp5oyAL2v5vSLGDs8p2CuohMmK5pCbyxwv09/xV6k5pTKC8pHlF0JCuB1Cna6rFdtNUOmgQgV+5/fQnWcwQPHYl+8DBTdvfGLPZwFZyCRLg1KBAKWAwgah4XxURnjT7Gfnquay7lOs0JA0RamOzKzTcMwT4YwdUKpqu8kYAcrgN1E9cOroPFOgdpTB8QjzQ4TFakNweOEFpV4qRcNYd7zgypS4Ic4/aEGrI4fiVikon1SckoFSEDX+4lOsvXxemon7i44RGhOr4CMKLDXR72frOebQObzEOaZgcqzjtHkVabxEpiSrHkPn/LdqduGsXc3vto/DXNSP1Dq0g+e66BeJUxoOrB+33W3KUzL6WQTaOkF7+EGBdT+V6AEhrIVXAij/q5LuMWeIUZSth8YaY3/TT6SQWMeA94AbwLnBdMI8utIxgK8vm2XnNuoN+bv6dWYr38uOpQfyuvlWR3qr6hOcKKpGgaUvXH6cK4gJR4dNdXzwYJRSNINzWd0o14G65QQEhO06ysjMXSxJ+VlKJPIiatcpHqYulXmvEK+KIvA5qm1aFLG9QG7H2Oe/kwNg7v3uNPoNIuwLEYukIjpa73ldx3cG71cNZJkI+vHCpxNx2Znn5/m1tKuLC/Nn8xuGGyFOPg2cb/aGOJK7RMidm8dub5xTivJkH1Xj8zYtaiEQgrJei8kVMZxEXhmXCr12fPfEcN0zzvxTmhK/kYEpE7HgnSvIpk61AitsQCPqOjwBLG1iqmEp9NGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":562,"cache_read_input_tokens":71580,"cache_creation":{"ephemeral_5m_input_tokens":562,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ce264f0c-2e99-4e06-9707-ee9c6ef84e96"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMDYmMNLCxt2uX8Cwze","type":"message","role":"assistant","content":[{"type":"text","text":"Mission terminée. Récapitulatif du dernier segment :\n\n- **CSS** : 166 lignes ajoutées à `frontend/src/styles.css` pour les 5 nouveaux blocs (pastilles `st-pill` observé/estimé/inconnu, tableau coût réel, timeline `hl-*`, barres de distribution d'avis `gb-*`, score hiver), alignées sur les tokens du design « éditorial sharp ».\n- **Build + déploiement** : `npm run build` OK, `pm2 restart lou-ka-web lou-ka-sync`, les deux online.\n- **Validation live** : les 5 endpoints répondent (`/historique`, `/cout-reel`, `/recyclees`, `/api/immeuble`, `/api/managers/logisco`), `immeuble`/`hiver` embarqués dans la fiche ; Playwright confirme l'affichage des 5 blocs (gestionnaire vérifié sur une annonce Logisco : ★ 3,6, 395 avis, distribution, plaintes récurrentes) et `check-order.mjs` valide l'ordre visuel desktop/mobile.\n- **Git** : une session ka2 parallèle avait déjà balayé le backend (8efd8cb) et les composants (0ee1a2d) dans ses snapshots ; j'ai vérifié qu'aucune clé SerpApi n'y figure (`.env.example` vide, zéro occurrence). Commit `521837e` (styles.css) poussé sur spbgit. `kangalou.py` modifié appartient à la session ka2 — laissé intact.\n\nLe dossier d'intelligence locative est complet et en production sur lou-ka.com."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":562,"cache_read_input_tokens":71580,"cache_creation":{"ephemeral_5m_input_tokens":562,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"926776a6-9640-454f-8abd-dd5224d07d0d"}} {"type":"claude","event":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2069173,"duration_api_ms":2015756,"num_turns":131,"result":"Mission terminée. Récapitulatif du dernier segment :\n\n- **CSS** : 166 lignes ajoutées à `frontend/src/styles.css` pour les 5 nouveaux blocs (pastilles `st-pill` observé/estimé/inconnu, tableau coût réel, timeline `hl-*`, barres de distribution d'avis `gb-*`, score hiver), alignées sur les tokens du design « éditorial sharp ».\n- **Build + déploiement** : `npm run build` OK, `pm2 restart lou-ka-web lou-ka-sync`, les deux online.\n- **Validation live** : les 5 endpoints répondent (`/historique`, `/cout-reel`, `/recyclees`, `/api/immeuble`, `/api/managers/logisco`), `immeuble`/`hiver` embarqués dans la fiche ; Playwright confirme l'affichage des 5 blocs (gestionnaire vérifié sur une annonce Logisco : ★ 3,6, 395 avis, distribution, plaintes récurrentes) et `check-order.mjs` valide l'ordre visuel desktop/mobile.\n- **Git** : une session ka2 parallèle avait déjà balayé le backend (8efd8cb) et les composants (0ee1a2d) dans ses snapshots ; j'ai vérifié qu'aucune clé SerpApi n'y figure (`.env.example` vide, zéro occurrence). Commit `521837e` (styles.css) poussé sur spbgit. `kangalou.py` modifié appartient à la session ka2 — laissé intact.\n\nLe dossier d'intelligence locative est complet et en production sur lou-ka.com.","stop_reason":"end_turn","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","total_cost_usd":8.194490799999999,"usage":{"input_tokens":214,"cache_creation_input_tokens":468265,"cache_read_input_tokens":11042274,"output_tokens":107689,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":468265},"inference_geo":"","iterations":[{"input_tokens":2,"output_tokens":821,"cache_read_input_tokens":71580,"cache_creation_input_tokens":562,"cache_creation":{"ephemeral_5m_input_tokens":562,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":15943,"outputTokens":20412,"cacheReadInputTokens":1189486,"cacheCreationInputTokens":100385,"webSearchRequests":1,"costUSD":0.37243285000000004,"contextWindow":200000,"maxOutputTokens":32000},"claude-fable-5":{"inputTokens":271499,"outputTokens":129259,"cacheReadInputTokens":11042274,"cacheCreationInputTokens":468265,"webSearchRequests":0,"costUSD":7.822057949999999,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"6c940bee-ba49-4e97-b485-9278c8e2fc3a"}} {"type":"status","state":"idle"} {"type":"user_prompt","text":"peux tu ajuster le rendu desktop et smart phone pour ces nouveau changements et aussi ce qu'il y a dans pdf de la fiche ca doit. etre sharp mon amis","ts":1787610152535} {"type":"status","state":"running"} {"type":"claude","event":{"type":"system","subtype":"init","cwd":"/Users/simon-pierreboucher/apps/lou-ka","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-fable-5","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","cost","heapdump","init","review","security-review","insights","team-onboarding"],"apiKeySource":"ANTHROPIC_API_KEY","claude_code_version":"2.1.117","output_style":"default","agents":["Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[],"uuid":"2208ddbd-9f29-48a8-9d33-51c074315806","memory_paths":{"auto":"/Users/simon-pierreboucher/.claude/projects/-Users-simon-pierreboucher-apps-lou-ka/memory/"},"fast_mode_state":"off"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"16b67b3f-6039-4ac4-8743-08acf8ded104","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMXExXwUVaU5DH2cjM6","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":57899,"cache_read_input_tokens":16221,"cache_creation":{"ephemeral_5m_input_tokens":57899,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e8845b9d-9628-4a65-89ef-cbab3144e20a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMXExXwUVaU5DH2cjM6","type":"message","role":"assistant","content":[{"type":"text","text":"Je regarde d'abord le rendu actuel (breakpoints fiche dans styles.css) et le générateur PDF pour voir ce qui manque."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":57899,"cache_read_input_tokens":16221,"cache_creation":{"ephemeral_5m_input_tokens":57899,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"50ad67ae-4089-47d2-8c7d-a3a5f516a622"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMXExXwUVaU5DH2cjM6","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012qMTvfimEJehq6zFexk6P8","name":"Bash","input":{"command":"grep -n \"@media\" frontend/src/styles.css | head -30; echo \"---f-bloc/f-col---\"; grep -n \"\\.f-col\\|\\.f-bloc {\\|\\.f-bloc,\\|\\.fiche \" frontend/src/styles.css | head -20","description":"Locate fiche breakpoints and f-bloc styles"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":57899,"cache_read_input_tokens":16221,"cache_creation":{"ephemeral_5m_input_tokens":57899,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"161a3e7b-c402-4b6b-85a9-b61535e7305c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012qMTvfimEJehq6zFexk6P8","type":"tool_result","content":"65:@media (max-width: 900px) { .brand-tag { display: none; } }\n85:@media (prefers-reduced-motion: reduce) {\n92:@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n129:@media (max-width: 640px) {\n288:@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n349:@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n373:@media (max-width: 900px) { .panel { position: static; } }\n379:@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n485:@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n515:@media (max-width: 640px) {\n549:@media (max-width: 640px) {\n624:@media (max-width: 760px) {\n647:@media (max-width: 900px) {\n653:@media (pointer: coarse) {\n681:@media (pointer: coarse) {\n707:@media (max-width: 780px) {\n811:@media (max-width: 760px) {\n839:@media (max-width: 640px) {\n863:@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n874:@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n900:@media (min-width: 900px) {\n937:@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n1006:@media (max-width: 640px) {\n1040:@media (min-width: 900px) { .cta-sticky { display: none; } }\n1041:@media (max-width: 899px) {\n1093:@media (prefers-reduced-motion: reduce) {\n1165:@media (max-width: 640px) {\n1226:@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n1386:@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n1480:@media (max-width: 780px) { .lmap3d { height: 280px; } }\n---f-bloc/f-col---\n898:.fiche { display: flex; flex-direction: column; gap: 22px; }\n899:.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n901: .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n902: .f-col { gap: 26px; }\n904:.f-bloc { min-width: 0; }","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bcf342aa-463a-462f-bfe1-6d695b28989d","timestamp":"2026-08-24T22:22:48.172Z","tool_use_result":{"stdout":"65:@media (max-width: 900px) { .brand-tag { display: none; } }\n85:@media (prefers-reduced-motion: reduce) {\n92:@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n129:@media (max-width: 640px) {\n288:@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n349:@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n373:@media (max-width: 900px) { .panel { position: static; } }\n379:@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n485:@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n515:@media (max-width: 640px) {\n549:@media (max-width: 640px) {\n624:@media (max-width: 760px) {\n647:@media (max-width: 900px) {\n653:@media (pointer: coarse) {\n681:@media (pointer: coarse) {\n707:@media (max-width: 780px) {\n811:@media (max-width: 760px) {\n839:@media (max-width: 640px) {\n863:@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n874:@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n900:@media (min-width: 900px) {\n937:@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n1006:@media (max-width: 640px) {\n1040:@media (min-width: 900px) { .cta-sticky { display: none; } }\n1041:@media (max-width: 899px) {\n1093:@media (prefers-reduced-motion: reduce) {\n1165:@media (max-width: 640px) {\n1226:@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n1386:@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n1480:@media (max-width: 780px) { .lmap3d { height: 280px; } }\n---f-bloc/f-col---\n898:.fiche { display: flex; flex-direction: column; gap: 22px; }\n899:.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n901: .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n902: .f-col { gap: 26px; }\n904:.f-bloc { min-width: 0; }","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMXExXwUVaU5DH2cjM6","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WtGcaziEEw57RLtaicFWq3","name":"Bash","input":{"command":"ls louka/pdf* 2>/dev/null; wc -l louka/pdfgen.py 2>/dev/null; grep -n \"def \\|SECTION\\|h2\\|coût\\|immeuble\\|hiver\\|gestionnaire\" louka/pdfgen.py | head -60","description":"Inspect pdfgen.py structure"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":57899,"cache_read_input_tokens":16221,"cache_creation":{"ephemeral_5m_input_tokens":57899,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d17aa49a-7563-457a-aeaf-aa1fdc1a0cdf"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WtGcaziEEw57RLtaicFWq3","type":"tool_result","content":"louka/pdfgen.py\n 1201 louka/pdfgen.py\n8:# historique TAL, inondation, qualité de l'air, coût d'électricité,\n11:# des loyers, types, villes, gestionnaires)\n65:def _fmt_money(v) -> str:\n69:def _fmt_dist(m_: float) -> str:\n74:def _marche(m_: float) -> str:\n78:def _fetch_image(url: str) -> ImageReader | None:\n102: def __init__(self, c: rl_canvas.Canvas):\n105: def fond(self):\n109: def logo(self, x: float, y: float, taille: float = 22):\n127: def entete(self, titre: str):\n141: def pied(self, texte: str, page: int | None = None):\n153: def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n169: def titre_section(self, y: float, texte: str) -> float:\n180: def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n207: def tuile(self, x: float, y: float, w: float, h: float,\n233:def _fmt_mois(d: str | None) -> str:\n250:_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n255:def fiche_pdf(uid: str) -> bytes | None:\n258: loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n299: def _safe(fn, *a, **k):\n332: def saut_page() -> float:\n339: def place(y_: float, besoin: float) -> float:\n587: def _rk(k: str) -> float:\n681: \"concerner un autre logement du même immeuble.\")\n802: # --- KA Scores (immeuble et environnement)\n897: # --- Références : QR + gestionnaire + source\n928:def s_titre_droite(c, y, x, texte):\n956:def _region_de(city: str) -> str:\n963:def rapport_pdf() -> bytes:\n995: (str(n_sources), \"gestionnaires connectés\"),\n1095: \"source originale du gestionnaire.\", 2)\n1167: # ---- page 4 : top villes + top gestionnaires\n1183: y = s.titre_section(y, \"Top 20 des gestionnaires\")","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6aa00e3a-ea08-4325-9be2-9826ecdf7007","timestamp":"2026-08-24T22:22:49.188Z","tool_use_result":{"stdout":"louka/pdfgen.py\n 1201 louka/pdfgen.py\n8:# historique TAL, inondation, qualité de l'air, coût d'électricité,\n11:# des loyers, types, villes, gestionnaires)\n65:def _fmt_money(v) -> str:\n69:def _fmt_dist(m_: float) -> str:\n74:def _marche(m_: float) -> str:\n78:def _fetch_image(url: str) -> ImageReader | None:\n102: def __init__(self, c: rl_canvas.Canvas):\n105: def fond(self):\n109: def logo(self, x: float, y: float, taille: float = 22):\n127: def entete(self, titre: str):\n141: def pied(self, texte: str, page: int | None = None):\n153: def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n169: def titre_section(self, y: float, texte: str) -> float:\n180: def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n207: def tuile(self, x: float, y: float, w: float, h: float,\n233:def _fmt_mois(d: str | None) -> str:\n250:_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n255:def fiche_pdf(uid: str) -> bytes | None:\n258: loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n299: def _safe(fn, *a, **k):\n332: def saut_page() -> float:\n339: def place(y_: float, besoin: float) -> float:\n587: def _rk(k: str) -> float:\n681: \"concerner un autre logement du même immeuble.\")\n802: # --- KA Scores (immeuble et environnement)\n897: # --- Références : QR + gestionnaire + source\n928:def s_titre_droite(c, y, x, texte):\n956:def _region_de(city: str) -> str:\n963:def rapport_pdf() -> bytes:\n995: (str(n_sources), \"gestionnaires connectés\"),\n1095: \"source originale du gestionnaire.\", 2)\n1167: # ---- page 4 : top villes + top gestionnaires\n1183: y = s.titre_section(y, \"Top 20 des gestionnaires\")","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"fa1fb703-7391-40fd-81b9-47d2b1e734f5","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMYT6omrQFFQAERZZRj","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS8AgKpwEIERgCKkBnh5Mta2sanK8QiWERqpnJb7NYPuolFdtvH3cq39C0476Lu/8mcWjrLKHZ6GeVCOlQrug/DoqEUqjLdnUNqUWuMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHCiLPUBrABAhIMivNXJXXq6+Tz+FGnGgwW7KCEIHNtgl+xGIEiMB08Xr50AJPSMJrqgSLW6BvGiBhEeVRYEp+uOFYPMlfWpZ8pslc5+B3cTf5u0o66Hyr1BsJX1KLnynAM78cxDzRmZMsQHSplS6YpYv+MVDgDm4QtVXbBlyBbf38+fOi+LCUWMVYuxtneX7OtZLRoov+ldRNVdsh/aSd607ediVkmGp7UIB6ISNor1EAiS2ygvX1ydUw5YZi4TkUgWJy4fQKIUFleu2EY4sHKzuQh2hkbBzlllcSzLG9cJTCuQR0qmnn9aB1nUaWNFxUODuoHZUBD4sxIi4Up1jHw/oJd3Txx60bhNh5yAfP051LoBvwbdt9L5ZHY7FSSqx54PzMKLz2SEF4eTWtQ9b40D/6QeZRVM/P8LvliOr5Y8uEVkN7JmIgtdvDZIYnNRjmrRuoEmECYus7cz/LpiDMl9cEXq/qRixTJpscXMK7Xbgk7d1tZl2RZDLvJtoamVjeOQl8j0BSTTCDy9cgdqh5Q94GYmbW+vkH900rlEZeiEO/2Ur3MpGYbgOm+MyJ//sI4R2aSRol0slqzoPS0BZyIE/qT1iIB3OyChA0+NmnPfKcu0svpjIN8S0LDdW8zXDKpmcN2H3LyCbHFFF2h+6ctSSTt4ARAprwu75VvoIUvW1cL+mGJ/eU0DekqXHvgTuOgkd92VOQ0lfwIygy3w0+XD2+dDgZoP+VIZi8XYBA1E0BYsea+jfuF2vC7HlFaKOlu8O7a+WG+jLpEGcwjrRRQ3igBlzXQ4+6EhK71dh3mftWGd782jqUVcOkVkBG30bwBykA7Up+lQ4vNmRfg8mU/R8SywmVa9s6s6ZmbGxSYLZRzOBnKpLq5msr92dhLFu2L/iVL9+/pHFQLdFd6WC5wuRjFkzOjFLjliR5ERcdnSaeAFI3NvPZ8VSgQm9wsoJh8xRLLCKI2i8AfqK3DucVDAiD6y47vnKv43B40uLvoZCuHuC0qmheuBS82Jx4Aa+qECps8eFV4tM/ByPVSTKZ7E8t8q6jr4Xt5bDAR2jC3bVv1HksWCqhUDNOoOTdP4DArDq8/soQdycHMN7Yl+HEZLSosLALj4K5IOvg6dAK2PSmJUzqAGbUdgQIhQ8jYA8oK2GRgQIwqx2ZmBEM+ZJKN9f4NnDrdd2lQxjrHKm0sJNt1dHiHXiHGtRvDFdX+STPY3uX10oomYWAa5CNCzoDOZ58JgCJBA1+GywIZqJth74Vlo1q/M478pEMBQkrX3RTD1rt9KCaywVttnVnAPRgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2699,"cache_read_input_tokens":74120,"cache_creation":{"ephemeral_5m_input_tokens":2699,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"aa3d1f83-2e6f-44ed-b3e8-87debaf9e2b6"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMYT6omrQFFQAERZZRj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01D76EwQt1X64hKAdKX8DtQN","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","offset":1,"limit":360},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2699,"cache_read_input_tokens":74120,"cache_creation":{"ephemeral_5m_input_tokens":2699,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"61ffb3e3-38d4-4626-ae35-47d7aa8d4c76"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01D76EwQt1X64hKAdKX8DtQN","type":"tool_result","content":"1\t# -----------------------------------------------------------------------------\n2\t# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n5\t# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n6\t# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n7\t# chips, description structurée, inclusions, registre des loyers,\n8\t# historique TAL, inondation, qualité de l'air, coût d'électricité,\n9\t# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n10\t# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n11\t# des loyers, types, villes, gestionnaires)\n12\t# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n13\t# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n14\t# par la mise en page — aucune dépendance de police externe.\n15\t# -----------------------------------------------------------------------------\n16\tfrom __future__ import annotations\n17\t\n18\timport io\n19\timport json\n20\timport math\n21\timport time\n22\t\n23\timport qrcode\n24\timport requests\n25\tfrom reportlab.lib.colors import HexColor\n26\tfrom reportlab.lib.pagesizes import letter\n27\tfrom reportlab.lib.utils import ImageReader\n28\tfrom reportlab.pdfgen import canvas as rl_canvas\n29\t\n30\tfrom . import db\n31\t\n32\t# --- palette Lou-Ka (identité officielle) ------------------------------------\n33\tPAPER = HexColor(\"#ffffff\")\n34\tSURFACE = HexColor(\"#ffffff\")\n35\tINK = HexColor(\"#0b1330\") # bleu marine profond\n36\tINK2 = HexColor(\"#3e4560\")\n37\tINK3 = HexColor(\"#8a90a5\")\n38\tGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\n39\tGREEN_DEEP = HexColor(\"#0b1330\")\n40\tLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\n41\tLIME_SOFT = HexColor(\"#fff1e6\")\n42\tAMBER = HexColor(\"#ff6a00\")\n43\tAMBER_SOFT = HexColor(\"#fff1e6\")\n44\t\n45\tPAGE_W, PAGE_H = letter\n46\tM = 40 # marge\n47\tIMG_TIMEOUT = 6\n48\tUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n49\t\n50\tPOI_LABELS = {\n51\t \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n52\t \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n53\t \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n54\t \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n55\t}\n56\tPROX_LABELS = [\n57\t (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n58\t (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n59\t (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n60\t]\n61\t\n62\tNBSP = \" \"\n63\t\n64\t\n65\tdef _fmt_money(v) -> str:\n66\t return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n67\t\n68\t\n69\tdef _fmt_dist(m_: float) -> str:\n70\t return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n71\t else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n72\t\n73\t\n74\tdef _marche(m_: float) -> str:\n75\t return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n76\t\n77\t\n78\tdef _fetch_image(url: str) -> ImageReader | None:\n79\t try:\n80\t r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n81\t r.raise_for_status()\n82\t raw = r.content\n83\t try:\n84\t # les photos source dépassent parfois 5 Mo chacune : sans\n85\t # redimensionnement le PDF grimpe à 30 Mo\n86\t from PIL import Image\n87\t im = Image.open(io.BytesIO(raw))\n88\t im.thumbnail((1200, 1200))\n89\t buf = io.BytesIO()\n90\t im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n91\t buf.seek(0)\n92\t return ImageReader(buf)\n93\t except Exception:\n94\t return ImageReader(io.BytesIO(raw))\n95\t except Exception:\n96\t return None\n97\t\n98\t\n99\tclass _Style:\n100\t \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n101\t\n102\t def __init__(self, c: rl_canvas.Canvas):\n103\t self.c = c\n104\t\n105\t def fond(self):\n106\t self.c.setFillColor(PAPER)\n107\t self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n108\t\n109\t def logo(self, x: float, y: float, taille: float = 22):\n110\t \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n111\t c = self.c\n112\t c.setFont(\"Helvetica-Bold\", taille)\n113\t c.setFillColor(INK)\n114\t c.drawString(x, y, \"Lou\")\n115\t w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n116\t bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n117\t c.saveState()\n118\t c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n119\t c.rotate(-3)\n120\t c.setFillColor(INK)\n121\t c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n122\t c.setFillColor(LIME)\n123\t c.drawCentredString(0, -taille * 0.30, \"Ka\")\n124\t c.restoreState()\n125\t return x + w + 6 + bw\n126\t\n127\t def entete(self, titre: str):\n128\t c = self.c\n129\t self.logo(M, PAGE_H - M - 16)\n130\t c.setFont(\"Courier-Bold\", 8)\n131\t c.setFillColor(INK3)\n132\t c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n133\t titre.upper())\n134\t c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n135\t time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n136\t c.setStrokeColor(INK)\n137\t c.setLineWidth(2)\n138\t c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n139\t return PAGE_H - M - 40 # y de départ du contenu\n140\t\n141\t def pied(self, texte: str, page: int | None = None):\n142\t c = self.c\n143\t c.setStrokeColor(INK)\n144\t c.setLineWidth(1)\n145\t c.line(M, M + 16, PAGE_W - M, M + 16)\n146\t c.setFont(\"Helvetica\", 6.5)\n147\t c.setFillColor(INK3)\n148\t c.drawString(M, M + 6, texte[:150])\n149\t if page:\n150\t c.setFont(\"Courier-Bold\", 7)\n151\t c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n152\t\n153\t def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n154\t stroke=None, taille: float = 8) -> float:\n155\t \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n156\t c = self.c\n157\t w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n158\t c.setFillColor(bg)\n159\t if stroke:\n160\t c.setStrokeColor(stroke)\n161\t c.setLineWidth(1.2)\n162\t c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n163\t stroke=1 if stroke else 0, fill=1)\n164\t c.setFillColor(fg)\n165\t c.setFont(\"Helvetica-Bold\", taille)\n166\t c.drawString(x + 7, y + 5, texte)\n167\t return x + w + 6\n168\t\n169\t def titre_section(self, y: float, texte: str) -> float:\n170\t c = self.c\n171\t c.setFont(\"Helvetica-Bold\", 12.5)\n172\t c.setFillColor(INK)\n173\t c.drawString(M, y, texte)\n174\t c.setStrokeColor(LIME)\n175\t c.setLineWidth(3)\n176\t w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n177\t c.line(M, y - 4, M + w, y - 4)\n178\t return y - 18\n179\t\n180\t def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n181\t taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n182\t max_lignes: int = 100) -> float:\n183\t \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n184\t c = self.c\n185\t c.setFont(\"Helvetica\", taille)\n186\t c.setFillColor(couleur)\n187\t mots = texte.split()\n188\t ligne = \"\"\n189\t n = 0\n190\t for mot in mots:\n191\t essai = (ligne + \" \" + mot).strip()\n192\t if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n193\t ligne = essai\n194\t else:\n195\t c.drawString(x, y, ligne)\n196\t y -= interligne\n197\t n += 1\n198\t ligne = mot\n199\t if n >= max_lignes - 1:\n200\t ligne += \" …\"\n201\t break\n202\t if ligne:\n203\t c.drawString(x, y, ligne)\n204\t y -= interligne\n205\t return y\n206\t\n207\t def tuile(self, x: float, y: float, w: float, h: float,\n208\t val: str, lab: str):\n209\t \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n210\t c = self.c\n211\t c.setFillColor(INK)\n212\t c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n213\t c.setFillColor(SURFACE)\n214\t c.setStrokeColor(INK)\n215\t c.setLineWidth(1.2)\n216\t c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n217\t c.setFont(\"Helvetica-Bold\", 12)\n218\t c.setFillColor(INK)\n219\t c.drawString(x + 8, y + h - 20, str(val))\n220\t c.setFont(\"Courier\", 6.2)\n221\t c.setFillColor(INK3)\n222\t c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n223\t\n224\t\n225\t# ---------------------------------------------------------------------------\n226\t# Fiche de propriété\n227\t# ---------------------------------------------------------------------------\n228\t\n229\t_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n230\t \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n231\t\n232\t\n233\tdef _fmt_mois(d: str | None) -> str:\n234\t \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n235\t if not d:\n236\t return \"—\"\n237\t try:\n238\t an, mois = d[:7].split(\"-\")\n239\t return f\"{_MOIS[int(mois) - 1]} {an}\"\n240\t except Exception:\n241\t return d\n242\t\n243\t\n244\t_VERDICTS = {\n245\t \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n246\t \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n247\t \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n248\t}\n249\t\n250\t_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n251\t \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n252\t \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n253\t \"valeur juridique.\")\n254\t\n255\tdef fiche_pdf(uid: str) -> bytes | None:\n256\t \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n257\t web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n258\t loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n259\t d'électricité, commerces/transport, essence, KA Scores, quartier,\n260\t à proximité, références + QR. None si introuvable.\"\"\"\n261\t con = db.connect()\n262\t row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n263\t if row is None:\n264\t con.close()\n265\t return None\n266\t l = dict(row)\n267\t l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n268\t l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n269\t l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n270\t l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n271\t poi = []\n272\t quartier = None\n273\t if l.get(\"lat\") is not None:\n274\t key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n275\t pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n276\t poi = json.loads(pr[\"pois\"]) if pr else []\n277\t from . import quartier as qmod\n278\t dauid = l.get(\"dauid\")\n279\t quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n280\t dauid if dauid and dauid != \"hors-zone\" else None)\n281\t reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n282\t src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n283\t kasc = None\n284\t if l.get(\"coord_key\"):\n285\t kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n286\t \" global AS glob FROM kascores WHERE coord_key=?\",\n287\t (l[\"coord_key\"],)).fetchone()\n288\t if kr is not None:\n289\t kasc = dict(kr)\n290\t price_history = [dict(r) for r in con.execute(\n291\t \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n292\t (uid,)).fetchall()]\n293\t con.close()\n294\t\n295\t # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n296\t lat, lng = l.get(\"lat\"), l.get(\"lng\")\n297\t geo = lat is not None and lng is not None\n298\t\n299\t def _safe(fn, *a, **k):\n300\t try:\n301\t return fn(*a, **k)\n302\t except Exception:\n303\t return None\n304\t\n305\t from . import air as air_mod\n306\t from . import commerces as com_mod\n307\t from . import fairvalue as fv_mod\n308\t from . import gaz as gaz_mod\n309\t from . import hydro as hydro_mod\n310\t from . import inondation as inon_mod\n311\t from . import rdl as rdl_mod\n312\t from . import tal as tal_mod\n313\t fv = _safe(fv_mod.explain, uid)\n314\t rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n315\t tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n316\t if l.get(\"address\") else None)\n317\t inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n318\t air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n319\t gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n320\t com = _safe(com_mod.nearby, lat, lng) if geo else None\n321\t hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n322\t uid=uid, lat=lat, lng=lng, solve=False)\n323\t\n324\t buf = io.BytesIO()\n325\t c = rl_canvas.Canvas(buf, pagesize=letter)\n326\t c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n327\t s = _Style(c)\n328\t s.fond()\n329\t y = s.entete(\"Fiche logement\")\n330\t page = [1]\n331\t\n332\t def saut_page() -> float:\n333\t s.pied(_PIED_FICHE, page[0])\n334\t c.showPage()\n335\t page[0] += 1\n336\t s.fond()\n337\t return s.entete(\"Fiche logement · suite\")\n338\t\n339\t def place(y_: float, besoin: float) -> float:\n340\t \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n341\t return saut_page() if (y_ - besoin) < M + 34 else y_\n342\t\n343\t # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n344\t prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n345\t else (l.get(\"price_label\") or \"Prix sur demande\")\n346\t c.setFont(\"Helvetica-Bold\", 30)\n347\t c.setFillColor(INK)\n348\t c.drawString(M, y - 24, prix_txt)\n349\t loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n350\t if quartier else None\n351\t bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n352\t if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n353\t dev = fv.get(\"deviation\") or 0\n354\t pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n355\t lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n356\t s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n357\t elif l.get(\"price\") and loyer_secteur:\n358\t delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n359\t pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n360\t if delta <= -0.15:"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"25f742e8-de2c-40d3-b7c3-49ccb5e803b4","timestamp":"2026-08-24T22:22:59.300Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","content":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:","numLines":360,"startLine":1,"totalLines":1202}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"3dcb91d2-991f-4347-99b5-a47acd57eae1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZCTXFZ9sN2jMuVjce","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS9gIKpwEIERgCKkBs4ZMRK/X7/+EefJgNZeKEMXToVrItErZ5GwEmKqZ6M82/GLeM8EnGiO+TkEj8Q1iJn1ZMmLKGWqFiSav2khldMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHHiLPUBrABAhIMOUfv/0Uu09eV0Ru1GgwMnkGXcgLrfWQZ4cMiMGWz/DgNOe3e08TVUFTB40XP3qwEeaEm5pHzEFfmd8SqLj6qZ3m9FmquC8jXnC5DNyp8+g/EhFyJ/VNFPZDgR2pqYmvGqfh5P7t9q7ac+zS7ixRoWalLRisPzcjqGs9OnqYkhlYMIn/ihhPD6OE3v07J4GoccI3d9ozc0MBbwDel4qs1yAcan2pNiUd7I8Y0EfdEfsGbFMptyDyHbHYviuYPxIAj+jh4bMcQNafeNhgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7810,"cache_read_input_tokens":76819,"cache_creation":{"ephemeral_5m_input_tokens":7810,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9d57e296-a151-4a54-9384-3c78b4760267"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZCTXFZ9sN2jMuVjce","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Cp8ejRw51HirMgnXVd6Q2d","name":"ToolSearch","input":{"query":"select:TodoWrite","max_results":1},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7810,"cache_read_input_tokens":76819,"cache_creation":{"ephemeral_5m_input_tokens":7810,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5216a04f-dd6b-4bc4-8ead-232cecb8bf83"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01Cp8ejRw51HirMgnXVd6Q2d","content":[{"type":"tool_reference","tool_name":"TodoWrite"}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"74c8f57c-7a5a-478a-9baf-a1fb5df502c7","timestamp":"2026-08-24T22:23:03.776Z","tool_use_result":{"matches":["TodoWrite"],"query":"select:TodoWrite","total_deferred_tools":17}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZCTXFZ9sN2jMuVjce","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Y41G5Gqk6amE2XYHRvjRC7","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","offset":360,"limit":340},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":7810,"cache_read_input_tokens":76819,"cache_creation":{"ephemeral_5m_input_tokens":7810,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bccf873c-2067-49de-a0cc-77aee601cd92"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Y41G5Gqk6amE2XYHRvjRC7","type":"tool_result","content":"360\t if delta <= -0.15:\n361\t s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n362\t elif delta <= 0.10:\n363\t s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n364\t else:\n365\t s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n366\t HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n367\t y -= 44\n368\t\n369\t # --- titre + adresse\n370\t c.setFont(\"Helvetica-Bold\", 13)\n371\t c.setFillColor(INK)\n372\t c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n373\t y -= 14\n374\t c.setFont(\"Helvetica\", 9)\n375\t c.setFillColor(INK2)\n376\t loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n377\t if x and x != l.get(\"title\"))\n378\t c.drawString(M, y, loc[:110])\n379\t y -= 20\n380\t\n381\t # --- chips clés\n382\t x = M\n383\t chips = []\n384\t if l.get(\"unit_type\"):\n385\t chips.append(l[\"unit_type\"])\n386\t ad = l.get(\"availability_date\")\n387\t if ad == \"now\":\n388\t chips.append(\"Libre maintenant\")\n389\t elif ad:\n390\t chips.append(f\"Dispo {ad}\")\n391\t if l.get(\"area_sqft\"):\n392\t chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n393\t if l.get(\"furnished\"):\n394\t chips.append(\"Meublé\")\n395\t if l.get(\"pets\"):\n396\t chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n397\t \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n398\t if l[\"details\"].get(\"floor\") is not None:\n399\t chips.append(f\"{l['details']['floor']}e étage\")\n400\t for chip in chips[:7]:\n401\t x = s.pilule(x, y - 6, chip)\n402\t y -= 30\n403\t\n404\t # --- photos : 1 grande + 3 vignettes\n405\t imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n406\t if imgs:\n407\t big_h = 170\n408\t big_w = big_h * 16 / 10.5\n409\t c.setStrokeColor(INK)\n410\t c.setLineWidth(1.5)\n411\t try:\n412\t c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n413\t preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n414\t except Exception:\n415\t pass\n416\t c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n417\t tx = M + big_w + 10\n418\t th = (big_h - 16) / 3\n419\t for i, im in enumerate(imgs[1:4]):\n420\t ty = y - (i + 1) * th - i * 8\n421\t try:\n422\t c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n423\t preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n424\t c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n425\t except Exception:\n426\t pass\n427\t y -= big_h + 18\n428\t\n429\t # --- « En bref » (digest) sur bandeau lime\n430\t dg = l.get(\"digest\")\n431\t if dg and dg.get(\"en_bref\"):\n432\t c.setFillColor(LIME_SOFT)\n433\t c.setStrokeColor(GREEN)\n434\t c.setLineWidth(1.2)\n435\t c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n436\t s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n437\t PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n438\t max_lignes=3)\n439\t y -= 54\n440\t\n441\t # --- description (2-3 sections max) | inclusions en colonne droite\n442\t col_g = PAGE_W * 0.56 - M\n443\t y_desc = s.titre_section(y, \"Description\")\n444\t if dg and dg.get(\"sections\"):\n445\t for sec in dg[\"sections\"][:3]:\n446\t c.setFont(\"Helvetica-Bold\", 9)\n447\t c.setFillColor(INK)\n448\t c.drawString(M, y_desc, sec[\"titre\"][:60])\n449\t y_desc -= 12\n450\t y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n451\t y_desc -= 4\n452\t if y_desc < 150:\n453\t break\n454\t elif l.get(\"description\"):\n455\t y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n456\t\n457\t xr = M + col_g + 24\n458\t y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n459\t d = l[\"details\"]\n460\t badges = []\n461\t inc = d.get(\"inclusions\") or {}\n462\t for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n463\t (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n464\t if inc.get(k):\n465\t badges.append(lab)\n466\t app = d.get(\"appliances\") or {}\n467\t if app.get(\"dishwasher\"):\n468\t badges.append(\"Lave-vaisselle\")\n469\t if app.get(\"washer_dryer\"):\n470\t badges.append(\"Laveuse-sécheuse\")\n471\t for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n472\t (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n473\t (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n474\t if d.get(k):\n475\t badges.append(lab)\n476\t if (d.get(\"parking\") or {}).get(\"available\"):\n477\t badges.append(\"Stationnement\")\n478\t if l.get(\"furnished\"):\n479\t badges.append(\"Meublé\")\n480\t c.setFont(\"Helvetica\", 8.5)\n481\t for b in badges[:12]:\n482\t c.setFillColor(GREEN)\n483\t c.drawString(xr, y_incl, \"✓\")\n484\t c.setFillColor(INK2)\n485\t c.drawString(xr + 12, y_incl, b)\n486\t y_incl -= 12\n487\t autres = [a for a in l[\"amenities\"]\n488\t if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n489\t for a in autres[: max(0, 14 - len(badges))]:\n490\t c.setFillColor(INK3)\n491\t c.drawString(xr + 12, y_incl, a[:38])\n492\t y_incl -= 11\n493\t\n494\t y = min(y_desc, y_incl) - 16\n495\t\n496\t # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n497\t if fv and fv.get(\"fv\"):\n498\t hist = fv.get(\"histogram\") or []\n499\t y = place(y, 168 if hist else 92)\n500\t y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n501\t jv = _fmt_money(round(fv[\"fv\"]))\n502\t c.setFont(\"Helvetica-Bold\", 17)\n503\t c.setFillColor(INK)\n504\t c.drawString(M, y - 10, jv)\n505\t c.setFont(\"Helvetica\", 8.5)\n506\t c.setFillColor(INK2)\n507\t c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n508\t \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n509\t meta = []\n510\t if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n511\t meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n512\t f\"{_fmt_money(round(fv['fv_high']))}\")\n513\t if fv.get(\"confidence\"):\n514\t meta.append(f\"confiance {fv['confidence']}\")\n515\t if fv.get(\"comps\"):\n516\t meta.append(f\"{fv['comps']} comparables\")\n517\t if fv.get(\"segment_n\"):\n518\t meta.append(f\"segment de {fv['segment_n']} annonces\")\n519\t c.setFont(\"Helvetica\", 8)\n520\t c.setFillColor(INK3)\n521\t c.drawString(M, y - 24, \" · \".join(meta)[:115])\n522\t if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n523\t dev = fv.get(\"deviation\") or 0\n524\t pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n525\t lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n526\t s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n527\t y -= 58\n528\t if hist:\n529\t ch_h = 40\n530\t lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n531\t larg = PAGE_W - 2 * M\n532\t base = y - ch_h - 6\n533\t if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n534\t bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n535\t bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n536\t c.setFillColor(LIME_SOFT)\n537\t c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n538\t max_n = max((h[\"n\"] for h in hist), default=1) or 1\n539\t bw = larg / len(hist)\n540\t c.setFillColor(GREEN)\n541\t for i, h in enumerate(hist):\n542\t bh = ch_h * h[\"n\"] / max_n\n543\t if bh > 0:\n544\t c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n545\t stroke=0, fill=1)\n546\t if hi > lo and l.get(\"price\"):\n547\t px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n548\t c.setStrokeColor(LIME)\n549\t c.setLineWidth(2)\n550\t c.line(px, base - 3, px, base + ch_h + 3)\n551\t c.setFont(\"Courier-Bold\", 6.5)\n552\t c.setFillColor(LIME)\n553\t c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n554\t base + ch_h + 6,\n555\t f\"CE LOYER · {_fmt_money(l['price'])}\")\n556\t c.setFont(\"Helvetica\", 6.5)\n557\t c.setFillColor(INK3)\n558\t c.drawString(M, base - 9, _fmt_money(lo))\n559\t c.drawRightString(PAGE_W - M, base - 9,\n560\t _fmt_money(hi) + \" — distribution des loyers du \"\n561\t \"segment de référence\")\n562\t y = base - 20\n563\t ph = [p for p in price_history if p.get(\"price\")]\n564\t if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n565\t c.setFont(\"Helvetica\", 8)\n566\t c.setFillColor(INK2)\n567\t chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n568\t c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n569\t y -= 16\n570\t\n571\t # --- Registre des loyers (déclarations citoyennes à proximité)\n572\t if rdl_d and rdl_d.get(\"n\"):\n573\t comp = rdl_d.get(\"items\") or []\n574\t y = place(y, 128 + min(len(comp), 8) * 12)\n575\t y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n576\t tw = (PAGE_W - 2 * M - 20) / 3\n577\t med_rec = rdl_d.get(\"median_recent\")\n578\t tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n579\t (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n580\t (_fmt_money(med_rec) if med_rec else \"—\",\n581\t f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n582\t for i, (val, lab) in enumerate(tuiles):\n583\t s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n584\t y -= 56\n585\t rooms = rdl_d.get(\"by_rooms\") or {}\n586\t if rooms:\n587\t def _rk(k: str) -> float:\n588\t try:\n589\t return float(k)\n590\t except ValueError:\n591\t return 99.0\n592\t parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n593\t for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n594\t c.setFont(\"Helvetica\", 8)\n595\t c.setFillColor(INK2)\n596\t c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n597\t y -= 14\n598\t if comp:\n599\t c.setFont(\"Courier-Bold\", 7)\n600\t c.setFillColor(INK3)\n601\t for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n602\t (\"DIST.\", 420), (\"LOYER\", 480)):\n603\t c.drawString(M + xoff, y, lab)\n604\t y -= 4\n605\t c.setStrokeColor(INK)\n606\t c.setLineWidth(1)\n607\t c.line(M, y, PAGE_W - M, y)\n608\t y -= 11\n609\t for it in comp[:8]:\n610\t c.setFont(\"Helvetica\", 8)\n611\t c.setFillColor(INK2)\n612\t c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n613\t c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n614\t c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n615\t c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n616\t c.setFont(\"Helvetica-Bold\", 8)\n617\t c.setFillColor(INK)\n618\t c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n619\t y -= 12\n620\t c.setFont(\"Helvetica\", 6.5)\n621\t c.setFillColor(INK3)\n622\t c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n623\t \"au Registre des loyers (registre-des-loyers.ca) — \"\n624\t \"données citoyennes non vérifiées.\")\n625\t y -= 16\n626\t\n627\t # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n628\t if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n629\t decs = (tal_d.get(\"decisions\") or [])[:6]\n630\t y = place(y, 70 + len(decs) * 24)\n631\t y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n632\t if tal_d[\"status\"] == \"pending\":\n633\t c.setFont(\"Helvetica\", 8.5)\n634\t c.setFillColor(INK2)\n635\t c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n636\t \"décisions publiées du TAL (résultat sur la \"\n637\t \"fiche en ligne sous peu).\")\n638\t y -= 18\n639\t elif not decs:\n640\t s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n641\t LIME_SOFT, GREEN)\n642\t c.setFont(\"Helvetica\", 8.5)\n643\t c.setFillColor(INK2)\n644\t c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n645\t \"repérée à cette adresse.\")\n646\t y -= 26\n647\t else:\n648\t n = tal_d.get(\"n\") or len(decs)\n649\t cl = tal_d.get(\"contre_locataire\") or 0\n650\t c.setFont(\"Helvetica-Bold\", 9)\n651\t c.setFillColor(INK)\n652\t c.drawString(M, y - 4,\n653\t f\"{n} décision{'s' if n > 1 else ''} publiée\"\n654\t f\"{'s' if n > 1 else ''} à cette adresse — \"\n655\t f\"{cl} à l'initiative du propriétaire.\")\n656\t y -= 20\n657\t for dec in decs:\n658\t qui = {\"locateur\": \"demande du propriétaire\",\n659\t \"locataire\": \"demande du locataire\"}.get(\n660\t dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n661\t c.setFont(\"Helvetica-Bold\", 8)\n662\t c.setFillColor(INK)\n663\t c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n664\t c.setFont(\"Helvetica\", 8)\n665\t c.setFillColor(INK2)\n666\t suite = qui\n667\t if dec.get(\"verdict\"):\n668\t suite += f\" · demande {dec['verdict']}\"\n669\t if dec.get(\"tags\"):\n670\t suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n671\t c.drawString(M + 78, y, suite[:88])\n672\t c.setFont(\"Courier-Bold\", 7)\n673\t c.setFillColor(GREEN)\n674\t c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n675\t y -= 12\n676\t y -= 6\n677\t c.setFont(\"Helvetica\", 6.5)\n678\t c.setFillColor(INK3)\n679\t c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n680\t \"par correspondance d'adresse — une décision peut \"\n681\t \"concerner un autre logement du même immeuble.\")\n682\t y -= 16\n683\t\n684\t # --- Risque d'inondation (BDZI)\n685\t if inon:\n686\t zones = (inon.get(\"zones\") or [])[:3]\n687\t y = place(y, 56 + len(zones) * 12)\n688\t y = s.titre_section(y, \"Risque d'inondation\")\n689\t statut = inon.get(\"statut\")\n690\t sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n691\t \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n692\t if statut == \"en_zone\":\n693\t s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n694\t HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n695\t elif statut == \"a_proximite\":\n696\t d0 = zones[0][\"distance_m\"] if zones else \"?\"\n697\t s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n698\t HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n699\t elif statut == \"hors_zone\":"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f371b6c8-9c5b-4885-b212-e5649ead60dd","timestamp":"2026-08-24T22:23:04.963Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","content":" if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":","numLines":340,"startLine":360,"totalLines":1202}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"96e264d3-0a9c-41a8-a81b-89f2f5370437","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZcsDJ2iL3cePDQETk","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISjgMKpwEIERgCKkA4gUO8trp8/yhOh2B7WuGg63KG4w0HvUeBTe/yd9Af4/n4iuLo1b+a9aNQgNEIxGVvCw9qp4S3aVNMhvrhTntUMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHNiLPUBrABAhIMt9fKvYlkVNxtlq4WGgx9b9275wRHaqPlphIiMM7CY4CvOUi8hGnQN5CaeYPJ62f3sHP6k3tW8vc2iAWKQvo/rgoPjKDnvdjWZv8x8yqTAeVnmoinhsCtQPJhvGZz9Mclv1Xf9PGDjlmb817g69tKaiWLBvVGlVO+t2OgYlF1c61D8UOuu3cVLooSLBiWp9UAnxcoBKmOakQf6p0EH9hVoV9JK4IJukspLi6tme4stZv55UDj+EGUSlR2SK5rjYOsP22mwf5Yv8AOpZHxAVuVLgbzkzQ6yoiGFn8gqIscv4phkhgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":10982,"cache_read_input_tokens":84629,"cache_creation":{"ephemeral_5m_input_tokens":10982,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3e2e289b-7a56-4353-bbce-8170b0877c2c"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZcsDJ2iL3cePDQETk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UBcxuvmrmSVzUS8tJrRDKt","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","offset":700,"limit":260},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":10982,"cache_read_input_tokens":84629,"cache_creation":{"ephemeral_5m_input_tokens":10982,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9a35d0ed-0472-4684-8e2b-eda3e5884d1c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UBcxuvmrmSVzUS8tJrRDKt","type":"tool_result","content":"700\t s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n701\t GREEN_DEEP, LIME_SOFT, GREEN)\n702\t else:\n703\t s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n704\t INK2, SURFACE, INK)\n705\t y -= 26\n706\t for z in zones:\n707\t c.setFont(\"Helvetica\", 8)\n708\t c.setFillColor(INK2)\n709\t c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n710\t + (f\" (rapport {z['date_rapport']})\"\n711\t if z.get(\"date_rapport\") else \"\"))\n712\t y -= 12\n713\t y -= 8\n714\t\n715\t # --- Qualité de l'air (RSQAQ)\n716\t if air_d and air_d.get(\"station\"):\n717\t mes = air_d.get(\"mesures\") or {}\n718\t y = place(y, 44 + len(mes) * 11)\n719\t y = s.titre_section(y, \"Qualité de l'air\")\n720\t c.setFont(\"Helvetica\", 8.5)\n721\t c.setFillColor(INK2)\n722\t c.drawString(M, y - 2,\n723\t f\"Station {air_d['station']} ({air_d['ville']}), à \"\n724\t + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n725\t \"du logement — moyennes annuelles :\")\n726\t y -= 16\n727\t for pol, m_ in list(mes.items())[:5]:\n728\t c.setFont(\"Helvetica-Bold\", 8)\n729\t c.setFillColor(INK)\n730\t c.drawString(M, y, pol)\n731\t c.setFont(\"Helvetica\", 8)\n732\t c.setFillColor(INK2)\n733\t val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n734\t txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n735\t if m_.get(\"ref\"):\n736\t txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n737\t c.drawString(M + 56, y, txt[:95])\n738\t y -= 11\n739\t y -= 8\n740\t\n741\t # --- Coût d'électricité (Hydro-Québec)\n742\t if hyd and hyd.get(\"disponible\"):\n743\t y = place(y, 52)\n744\t y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n745\t c.setFont(\"Helvetica-Bold\", 15)\n746\t c.setFillColor(INK)\n747\t mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n748\t c.drawString(M, y - 8, mens)\n749\t c.setFont(\"Helvetica\", 8.5)\n750\t c.setFillColor(INK2)\n751\t suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n752\t if hyd.get(\"kwh_annuel\"):\n753\t suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n754\t suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n755\t c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n756\t y -= 26\n757\t\n758\t # --- Commerces et transport\n759\t if com and (com.get(\"commerces\") or com.get(\"transit\")):\n760\t entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n761\t for t in (com.get(\"transit\") or [])[:4]]\n762\t + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n763\t for i in (com.get(\"commerces\") or [])[:10]])\n764\t lignes = (len(entrees) + 1) // 2\n765\t y = place(y, 36 + lignes * 13)\n766\t y = s.titre_section(y, \"Commerces et transport\")\n767\t col_w = (PAGE_W - 2 * M) / 2\n768\t for i, (cat, nom, dist) in enumerate(entrees):\n769\t px = M if i % 2 == 0 else M + col_w + 10\n770\t py = y - (i // 2) * 13\n771\t c.setFont(\"Helvetica\", 8)\n772\t c.setFillColor(INK2)\n773\t nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n774\t c.drawString(px, py, f\"{cat} — {nomc}\")\n775\t c.setFont(\"Courier-Bold\", 7.5)\n776\t c.setFillColor(INK)\n777\t c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n778\t y -= lignes * 13 + 12\n779\t\n780\t # --- Essence à proximité\n781\t if gaz_d and gaz_d.get(\"stations\"):\n782\t sts = gaz_d[\"stations\"][:3]\n783\t y = place(y, 40 + len(sts) * 11)\n784\t y = s.titre_section(y, \"Essence à proximité\")\n785\t if gaz_d.get(\"mediane_regulier\"):\n786\t c.setFont(\"Helvetica\", 8.5)\n787\t c.setFillColor(INK2)\n788\t c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n789\t + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n790\t + f\"{NBSP}¢/L\")\n791\t y -= 14\n792\t for st in sts:\n793\t c.setFont(\"Helvetica\", 8)\n794\t c.setFillColor(INK2)\n795\t reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n796\t if st.get(\"regulier\") else \"—\")\n797\t c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n798\t + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n799\t y -= 11\n800\t y -= 8\n801\t\n802\t # --- KA Scores (immeuble et environnement)\n803\t if kasc and kasc.get(\"glob\") is not None:\n804\t from . import kascores as ks_mod\n805\t y = place(y, 92)\n806\t y = s.titre_section(y, \"KA Scores\")\n807\t paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n808\t (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n809\t (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n810\t tw = (PAGE_W - 2 * M - 5 * 8) / 6\n811\t for i, (lab, v) in enumerate(paires):\n812\t val = \"—\" if v is None else str(round(v))\n813\t sub = ks_mod.label(v) or \"\"\n814\t s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n815\t f\"{lab} · {sub}\" if sub else lab)\n816\t y -= 62\n817\t\n818\t # --- Le quartier\n819\t if quartier:\n820\t y = place(y, 270)\n821\t y = s.titre_section(y, \"Le quartier\")\n822\t demo = quartier.get(\"demographie\") or {}\n823\t tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n824\t (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n825\t (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n826\t (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n827\t (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n828\t (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n829\t tw = (PAGE_W - 2 * M - 20) / 3\n830\t for i, (lab, val) in enumerate(tuiles):\n831\t s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n832\t tw, 38, val, lab)\n833\t y -= 34 + 44 + 16\n834\t\n835\t prox = quartier.get(\"proximite\") or {}\n836\t for k, lab in PROX_LABELS:\n837\t if k not in prox:\n838\t continue\n839\t v = max(0.0, min(1.0, prox[k]))\n840\t c.setFont(\"Helvetica\", 8)\n841\t c.setFillColor(INK2)\n842\t c.drawString(M, y, lab)\n843\t bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n844\t c.setFillColor(SURFACE)\n845\t c.setStrokeColor(INK)\n846\t c.setLineWidth(0.8)\n847\t c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n848\t c.setFillColor(GREEN)\n849\t c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n850\t c.setFont(\"Courier-Bold\", 7.5)\n851\t c.setFillColor(INK)\n852\t c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n853\t y -= 14\n854\t y -= 4\n855\t badges_q = []\n856\t ch = quartier.get(\"chaleur\")\n857\t if ch:\n858\t if ch[\"classe\"] <= 3:\n859\t badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n860\t elif ch[\"classe\"] >= 7:\n861\t badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n862\t HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n863\t cr = quartier.get(\"crime\")\n864\t if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n865\t dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n866\t badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n867\t f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n868\t INK2, SURFACE, INK))\n869\t elif cr and cr.get(\"type\") == \"points\":\n870\t badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n871\t INK2, SURFACE, INK))\n872\t x = M\n873\t for txt, fg, bg, stk in badges_q:\n874\t x = s.pilule(x, y - 8, txt, fg, bg, stk)\n875\t y -= 30\n876\t\n877\t # --- À proximité (commodités du cache POI)\n878\t if poi:\n879\t lignes = (min(len(poi), 12) + 1) // 2\n880\t y = place(y, 36 + lignes * 13)\n881\t y = s.titre_section(y, \"À proximité\")\n882\t col_w = (PAGE_W - 2 * M) / 2\n883\t for i, p in enumerate(poi[:12]):\n884\t px = M if i % 2 == 0 else M + col_w + 10\n885\t py = y - (i // 2) * 13\n886\t c.setFont(\"Helvetica\", 8)\n887\t c.setFillColor(INK2)\n888\t lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n889\t nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n890\t c.drawString(px, py, f\"{lab} — {nom_poi}\")\n891\t c.setFont(\"Courier-Bold\", 7.5)\n892\t c.setFillColor(INK)\n893\t c.drawRightString(px + col_w - 14, py,\n894\t f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n895\t y -= lignes * 13 + 14\n896\t\n897\t # --- Références : QR + gestionnaire + source\n898\t y = place(y, 120)\n899\t y = s.titre_section(y, \"Références\")\n900\t qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n901\t qb = io.BytesIO()\n902\t qr.save(qb, format=\"PNG\")\n903\t c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n904\t c.setFont(\"Helvetica-Bold\", 9)\n905\t c.setFillColor(INK)\n906\t c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n907\t c.setFont(\"Helvetica\", 8)\n908\t c.setFillColor(INK2)\n909\t c.drawString(M + 82, y - 28, \"Annonce originale :\")\n910\t c.setFillColor(GREEN)\n911\t c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n912\t c.setFillColor(INK2)\n913\t c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n914\t c.setFillColor(GREEN)\n915\t c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n916\t y -= 86\n917\t c.setFont(\"Helvetica\", 6.5)\n918\t c.setFillColor(INK3)\n919\t c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n920\t \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n921\t \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n922\t\n923\t s.pied(_PIED_FICHE, page[0])\n924\t c.save()\n925\t return buf.getvalue()\n926\t\n927\t\n928\tdef s_titre_droite(c, y, x, texte):\n929\t c.setFont(\"Helvetica-Bold\", 12.5)\n930\t c.setFillColor(INK)\n931\t c.drawString(x, y, texte)\n932\t c.setStrokeColor(LIME)\n933\t c.setLineWidth(3)\n934\t c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n935\t return y - 18\n936\t\n937\t\n938\t# ---------------------------------------------------------------------------\n939\t# Rapport global du marché\n940\t# ---------------------------------------------------------------------------\n941\t\n942\t_REGIONS = [\n943\t (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n944\t (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n945\t (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n946\t (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n947\t (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n948\t (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n949\t (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n950\t (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n951\t (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n952\t (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n953\t]\n954\t\n955\t\n956\tdef _region_de(city: str) -> str:\n957\t for nom, villes in _REGIONS:\n958\t if city in villes:\n959\t return nom"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"14198dbd-f11a-40ae-b626-55fb7828fcc2","timestamp":"2026-08-24T22:23:09.519Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","content":" s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom","numLines":260,"startLine":700,"totalLines":1202}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"494b4e7b-4c97-4835-ac9f-1b69f2cf1c64","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZxNifqDgLJLxjJ4Tv","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6638,"cache_read_input_tokens":95611,"cache_creation":{"ephemeral_5m_input_tokens":6638,"ephemeral_1h_input_tokens":0},"output_tokens":7,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a72ad545-d205-46f8-a505-a1f61f1ce58b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZxNifqDgLJLxjJ4Tv","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MPp7LFPtsbNh62iqB7h9om","name":"TodoWrite","input":{"todos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"in_progress","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"pending","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"pending","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6638,"cache_read_input_tokens":95611,"cache_creation":{"ephemeral_5m_input_tokens":6638,"ephemeral_1h_input_tokens":0},"output_tokens":7,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3565cf89-abf6-4211-8aa4-c1e78a59e7b2"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MPp7LFPtsbNh62iqB7h9om","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3379afc8-4049-4353-91f7-5009e8ea18a7","timestamp":"2026-08-24T22:23:33.117Z","tool_use_result":{"oldTodos":[],"newTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"in_progress","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"pending","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"pending","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMZxNifqDgLJLxjJ4Tv","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XHVfbxmCfrNqNoespdYqcx","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","offset":1,"limit":40},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6638,"cache_read_input_tokens":95611,"cache_creation":{"ephemeral_5m_input_tokens":6638,"ephemeral_1h_input_tokens":0},"output_tokens":7,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"aaed5046-e95c-4791-ac78-7ea38fad8f76"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XHVfbxmCfrNqNoespdYqcx","type":"tool_result","content":"1\t/* -----------------------------------------------------------------------------\n2\t Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n5\t Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n6\t ce fichier dans main.tsx).\n7\t · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n8\t · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n9\t · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n10\t · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n11\t · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n12\t · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n13\t----------------------------------------------------------------------------- */\n14\t:root {\n15\t /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n16\t --accent: #ff6a00;\n17\t --accent-soft: #fff1e6;\n18\t --accent-deep: #cc5500;\n19\t --on-accent: #ffffff;\n20\t --lime: var(--accent);\n21\t --lime-soft: var(--accent-soft);\n22\t\n23\t /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n24\t les jetons communs (le fond redevient papier via tokens.css) ---- */\n25\t --navy: var(--ink);\n26\t --navy-soft: var(--surface-2);\n27\t --blue: var(--green);\n28\t --blue-soft: #e8f0ea;\n29\t --orange: var(--accent);\n30\t --orange-soft: var(--accent-soft);\n31\t --orange-deep: var(--accent-deep);\n32\t --grey: var(--surface-2);\n33\t --white: #ffffff;\n34\t}\n35\t\n36\t#root { position: relative; z-index: 1; }\n37\ta { text-decoration: none; }\n38\tbutton { font-family: inherit; }\n39\t\n40\t.mono { font-family: var(--font-mono); }"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2cdd9ec0-ace9-43f2-990d-ce73dc71439a","timestamp":"2026-08-24T22:23:33.978Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","content":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }","numLines":40,"startLine":1,"totalLines":2282}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"67c4df91-6bad-461f-a469-4372641e91d2","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMbkvx4E8fFSmVRmakh","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2556,"cache_read_input_tokens":102249,"cache_creation":{"ephemeral_5m_input_tokens":2556,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e3a94391-5558-40d3-b2ac-90ddf817f437"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMbkvx4E8fFSmVRmakh","type":"message","role":"assistant","content":[{"type":"text","text":"Je lis le markup de CoutReel pour ajuster le CSS précisément, puis je fais la passe responsive/sharp."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2556,"cache_read_input_tokens":102249,"cache_creation":{"ephemeral_5m_input_tokens":2556,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"13e83f93-1ed2-43e0-a111-aa75c91a764a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMbkvx4E8fFSmVRmakh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011132Wt91iLxtxWsaFoHQsB","name":"Read","input":{"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/CoutReel.tsx"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2556,"cache_read_input_tokens":102249,"cache_creation":{"ephemeral_5m_input_tokens":2556,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"96907295-6362-48c2-90ee-fe54319e38d1"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011132Wt91iLxtxWsaFoHQsB","type":"tool_result","content":"1\t// -----------------------------------------------------------------------------\n2\t// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n3\t// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n4\t// components/CoutReel.tsx : bloc « Coût réel mensuel » (fiche)\n5\t// Loyer + frais non inclus, ligne par ligne, chaque poste étiqueté :\n6\t// inclus (bail) / observé (source) / estimé (avec sa source) / inconnu\n7\t// (dit tel quel — jamais chiffré arbitrairement). Prix au pi² avec\n8\t// percentile réellement calculé sur les comparables.\n9\t// -----------------------------------------------------------------------------\n10\timport { useEffect, useState } from \"react\";\n11\timport { CoutReel as CoutReelT, fetchCoutReel, fmtPrice } from \"../api\";\n12\t\n13\tconst NBSP = \" \";\n14\t\n15\tconst STATUT_META: Record = {\n16\t included: { label: \"inclus\", cls: \"st-included\" },\n17\t observed: { label: \"observé\", cls: \"st-observed\" },\n18\t estimated: { label: \"estimé\", cls: \"st-estimated\" },\n19\t unknown: { label: \"inconnu\", cls: \"st-unknown\" },\n20\t};\n21\t\n22\texport default function CoutReel({ uid }: { uid: string }) {\n23\t const [d, setD] = useState(null);\n24\t useEffect(() => {\n25\t setD(null);\n26\t fetchCoutReel(uid).then(setD).catch(() => setD(null));\n27\t }, [uid]);\n28\t if (!d || d.loyer == null) return null;\n29\t\n30\t const pi2 = d.pi2;\n31\t return (\n32\t
\n33\t

Coût réel mensuel

\n34\t
\n35\t \n36\t {d.lignes.map((li) => {\n37\t const m = STATUT_META[li.statut] ?? STATUT_META.unknown;\n38\t return (\n39\t \n40\t \n44\t \n49\t \n50\t );\n51\t })}\n52\t \n53\t {d.total_estime != null && (\n54\t \n55\t \n56\t \n57\t \n58\t \n59\t {d.annuel_estime != null && (\n60\t \n61\t \n62\t \n63\t \n64\t )}\n65\t \n66\t )}\n67\t
\n41\t {li.poste}\n42\t {m.label}\n43\t \n45\t {li.montant != null && li.montant > 0 && fmtPrice(li.montant)}\n46\t {li.montant === 0 && li.statut === \"included\" && `0${NBSP}$`}\n47\t {li.montant == null && \"—\"}\n48\t
Total estimé≈{NBSP}{fmtPrice(d.total_estime)}{NBSP}/mois
soit sur 12 mois≈{NBSP}{fmtPrice(d.annuel_estime)}
\n68\t {d.postes_inconnus.length > 0 && (\n69\t

\n70\t Postes non chiffrables avec les données publiées :{\" \"}\n71\t {d.postes_inconnus.join(\", \").toLowerCase()} — le total réel peut\n72\t être plus élevé.\n73\t

\n74\t )}\n75\t {pi2 && (\n76\t
\n77\t \n78\t {pi2.valeur.toFixed(2).replace(\".\", \",\")}{NBSP}$/pi²\n79\t \n80\t {pi2.percentile_secteur != null && (\n81\t \n82\t {\" \"}moins cher que {100 - pi2.percentile_secteur}{NBSP}% des{\" \"}\n83\t {pi2.n_secteur} logements comparables du secteur (~2{NBSP}km)\n84\t \n85\t )}\n86\t {pi2.percentile_secteur == null && pi2.percentile_ville != null && (\n87\t \n88\t {\" \"}moins cher que {100 - pi2.percentile_ville}{NBSP}% des{\" \"}\n89\t {pi2.n_ville} comparables ({pi2.portee_ville})\n90\t \n91\t )}\n92\t {pi2.percentile_note && {pi2.percentile_note}}\n93\t
\n94\t )}\n95\t

{d.methode}

\n96\t \n97\t );\n98\t}\n99\t"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"205b629e-4e01-410e-8c25-bb37c8c452aa","timestamp":"2026-08-24T22:23:51.708Z","tool_use_result":{"type":"text","file":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/components/CoutReel.tsx","content":"// -----------------------------------------------------------------------------\n// Lou-Ka — Agrégateur de logements à louer (province de Québec)\n// Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n// components/CoutReel.tsx : bloc « Coût réel mensuel » (fiche)\n// Loyer + frais non inclus, ligne par ligne, chaque poste étiqueté :\n// inclus (bail) / observé (source) / estimé (avec sa source) / inconnu\n// (dit tel quel — jamais chiffré arbitrairement). Prix au pi² avec\n// percentile réellement calculé sur les comparables.\n// -----------------------------------------------------------------------------\nimport { useEffect, useState } from \"react\";\nimport { CoutReel as CoutReelT, fetchCoutReel, fmtPrice } from \"../api\";\n\nconst NBSP = \" \";\n\nconst STATUT_META: Record = {\n included: { label: \"inclus\", cls: \"st-included\" },\n observed: { label: \"observé\", cls: \"st-observed\" },\n estimated: { label: \"estimé\", cls: \"st-estimated\" },\n unknown: { label: \"inconnu\", cls: \"st-unknown\" },\n};\n\nexport default function CoutReel({ uid }: { uid: string }) {\n const [d, setD] = useState(null);\n useEffect(() => {\n setD(null);\n fetchCoutReel(uid).then(setD).catch(() => setD(null));\n }, [uid]);\n if (!d || d.loyer == null) return null;\n\n const pi2 = d.pi2;\n return (\n
\n

Coût réel mensuel

\n \n \n {d.lignes.map((li) => {\n const m = STATUT_META[li.statut] ?? STATUT_META.unknown;\n return (\n \n \n \n \n );\n })}\n \n {d.total_estime != null && (\n \n \n \n \n \n {d.annuel_estime != null && (\n \n \n \n \n )}\n \n )}\n
\n {li.poste}\n {m.label}\n \n {li.montant != null && li.montant > 0 && fmtPrice(li.montant)}\n {li.montant === 0 && li.statut === \"included\" && `0${NBSP}$`}\n {li.montant == null && \"—\"}\n
Total estimé≈{NBSP}{fmtPrice(d.total_estime)}{NBSP}/mois
soit sur 12 mois≈{NBSP}{fmtPrice(d.annuel_estime)}
\n {d.postes_inconnus.length > 0 && (\n

\n Postes non chiffrables avec les données publiées :{\" \"}\n {d.postes_inconnus.join(\", \").toLowerCase()} — le total réel peut\n être plus élevé.\n

\n )}\n {pi2 && (\n
\n \n {pi2.valeur.toFixed(2).replace(\".\", \",\")}{NBSP}$/pi²\n \n {pi2.percentile_secteur != null && (\n \n {\" \"}moins cher que {100 - pi2.percentile_secteur}{NBSP}% des{\" \"}\n {pi2.n_secteur} logements comparables du secteur (~2{NBSP}km)\n \n )}\n {pi2.percentile_secteur == null && pi2.percentile_ville != null && (\n \n {\" \"}moins cher que {100 - pi2.percentile_ville}{NBSP}% des{\" \"}\n {pi2.n_ville} comparables ({pi2.portee_ville})\n \n )}\n {pi2.percentile_note && {pi2.percentile_note}}\n
\n )}\n

{d.methode}

\n
\n );\n}\n","numLines":99,"startLine":1,"totalLines":99}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"26f53507-445f-4c10-898e-bb25f5e3e412","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMd4hZN8aer28XBLFco","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2672,"cache_read_input_tokens":104805,"cache_creation":{"ephemeral_5m_input_tokens":2672,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5d4b3251-955c-44d6-a34c-ab6402f51640"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMd4hZN8aer28XBLFco","type":"message","role":"assistant","content":[{"type":"text","text":"Je fais la passe « sharp + responsive » sur le CSS des nouveaux blocs."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2672,"cache_read_input_tokens":104805,"cache_creation":{"ephemeral_5m_input_tokens":2672,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fa949aa1-0434-4c29-94c4-9e80fe3ac76b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMd4hZN8aer28XBLFco","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SB9JzBvztiNQAtDFz4LENF","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","old_string":".st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1px solid currentColor; border-radius: var(--r-pill, 999px);\n white-space: nowrap;\n}","new_string":".st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2672,"cache_read_input_tokens":104805,"cache_creation":{"ephemeral_5m_input_tokens":2672,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3c415650-5b4f-4d7c-9c5c-d886b7fa4b86"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SB9JzBvztiNQAtDFz4LENF","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c6d907c4-9f8c-4962-b5e3-cda565012b21","timestamp":"2026-08-24T22:24:08.828Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","oldString":".st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1px solid currentColor; border-radius: var(--r-pill, 999px);\n white-space: nowrap;\n}","newString":".st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }","originalFile":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }\n\n/* Garde-fou : jamais de défilement horizontal de la page, même transitoire\n pendant le chargement (bandeau défilant, polices) — clip ne casse pas\n position: sticky, contrairement à hidden. */\nhtml, body { overflow-x: clip; }\n\n/* ================= Header ================= */\n.header {\n position: sticky; top: 0; z-index: var(--z-header, 500);\n background: var(--paper);\n border-bottom: 1px solid var(--line);\n}\n.header-inner { display: flex; align-items: center; gap: 20px; height: 64px; }\n.brand {\n font-family: var(--font-display); font-weight: 700; font-size: 25px;\n letter-spacing: -0.03em; display: flex; align-items: center; line-height: 1;\n color: var(--navy);\n}\n.brand .logo-ico { margin-right: 12px; flex: none; } /* ≈ 40 % de la largeur de l'icône */\n.brand .ka { color: var(--orange); }\n.brand-tag {\n font-size: 11px; font-weight: 500; color: var(--ink-3);\n letter-spacing: 0.02em; margin-left: 12px;\n}\n@media (max-width: 900px) { .brand-tag { display: none; } }\n.nav { margin-left: auto; display: flex; gap: 4px; }\n.nav a {\n padding: 9px 16px; border-radius: var(--r-pill); font-weight: 600; font-size: 14px;\n color: var(--navy); transition: all 0.15s ease; min-height: 40px;\n display: inline-flex; align-items: center;\n}\n.nav a:hover { color: var(--orange); background: var(--orange-soft); }\n.nav a.active { background: var(--navy); color: var(--white); }\n\n/* ================= Ticker ================= */\n.ticker {\n background: var(--navy); color: rgba(255, 255, 255, 0.85); overflow: hidden;\n font-size: 11.5px; font-weight: 500; letter-spacing: 0.08em;\n text-transform: uppercase; padding: 7px 0; white-space: nowrap;\n}\n.ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }\n.ticker span { padding: 0 26px; position: relative; }\n.ticker span::after { content: \"◆\"; position: absolute; right: -6px; color: var(--orange); opacity: 0.8; font-size: 8px; top: 3px; }\n@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) {\n .ticker-track { animation: none; }\n * { transition-duration: 0.01ms !important; }\n}\n\n/* ================= Hero ================= */\n.hero { padding: 58px 0 22px; }\n@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n.hero h1 {\n font-size: clamp(36px, 5.4vw, 56px); font-weight: 700; line-height: 1.06;\n letter-spacing: -0.03em; max-width: 900px; margin-top: 14px; color: var(--navy);\n}\n.hero h1 .outline { color: var(--navy); }\n.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }\n.stat-chip {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 8px 16px; font-size: 12.5px; font-weight: 500;\n color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n box-shadow: var(--shadow-flat);\n}\n.stat-chip b { color: var(--navy); font-weight: 700; }\n.stat-chip .pulse {\n width: 8px; height: 8px; border-radius: 50%; background: var(--orange);\n box-shadow: 0 0 0 4px var(--orange-soft); animation: pulse 2.4s ease infinite;\n}\n@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 106, 0, 0.18); } }\n\n/* ================= Rangée d'avantages ================= */\n.benefits {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px; margin-top: 30px;\n}\n.benefit { display: flex; align-items: center; gap: 12px; }\n.benefit-ico {\n width: 42px; height: 42px; border-radius: var(--r-pill); flex: none;\n display: flex; align-items: center; justify-content: center;\n background: var(--orange-soft); color: var(--orange);\n}\n.benefit-ico.blue { background: var(--blue-soft); color: var(--blue); }\n.benefit-label {\n font-size: 11.5px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--navy);\n}\n@media (max-width: 640px) {\n .benefits { grid-template-columns: 1fr 1fr; gap: 12px; }\n}\n\n/* ================= Filter bar ================= */\n.filterbar {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 14px; margin: 30px 0 6px;\n display: flex; flex-direction: column; gap: 0; min-width: 0;\n}\n\n/* — rangée principale — */\n.f-primary { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; min-width: 0; }\n.f-search {\n flex: 1 1 220px; min-width: 0; max-width: 440px; align-self: center;\n display: flex; align-items: center; gap: 8px;\n border: 1px solid transparent; border-radius: var(--r-pill); background: var(--grey);\n padding: 0 15px; min-height: 42px; color: var(--ink-2);\n transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;\n}\n.f-search:focus-within {\n background: var(--white); border-color: var(--blue);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n.f-search input {\n border: none; background: none; outline: none; flex: 1; min-width: 0;\n font-size: 14px; color: var(--ink); font-family: inherit;\n}\n.f-clear {\n border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n width: 20px; height: 20px; font-size: 10px; cursor: pointer; flex: none;\n display: grid; place-items: center;\n}\n.f-ctl {\n flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center;\n gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n padding: 7px 14px 6px; min-height: 52px; cursor: pointer;\n transition: box-shadow 0.15s ease, border-color 0.15s ease;\n}\n.f-ctl:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.f-ctl > span {\n font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-ctl select {\n border: none; background: transparent; outline: none; font-family: var(--font-display);\n font-weight: 700; font-size: 14.5px; color: var(--navy); cursor: pointer;\n appearance: none; -webkit-appearance: none; padding-right: 16px; min-width: 0; max-width: 170px;\n text-overflow: ellipsis;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right center;\n}\n.range-pair { display: flex; align-items: center; gap: 4px; }\n.range-pair select { max-width: 86px; }\n.range-sep { color: var(--ink-3); font-size: 11px; }\n.f-more {\n flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n transition: all 0.13s ease;\n}\n.f-more:hover { background: var(--blue-soft); border-color: var(--blue); color: var(--blue); }\n.f-more.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* — panneau avancé — */\n.f-adv {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 22px;\n border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n animation: adv-in 0.18s ease;\n}\n@keyframes adv-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }\n.f-group { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.f-group > label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-group-end { justify-content: flex-end; }\n.f-group .btn:disabled { opacity: 0.4; cursor: default; }\n.f-native {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 10px 30px 10px 14px; font-size: 14px; color: var(--ink); outline: none;\n font-family: inherit; min-height: 42px; width: 100%; min-width: 0;\n appearance: none; -webkit-appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center;\n}\n.f-native:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); background: var(--white); }\n\n/* segments (pilules soudées) */\n.seg { display: inline-flex; flex-wrap: wrap; row-gap: 6px; }\n.seg button {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink-2);\n padding: 8px 14px; font-family: var(--font-display); font-weight: 600; font-size: 13px;\n cursor: pointer; margin-left: -1px; white-space: nowrap; min-height: 38px;\n transition: all 0.12s ease;\n}\n.seg button:first-child { border-radius: var(--r-pill) 0 0 var(--r-pill); margin-left: 0; padding-left: 16px; }\n.seg button:last-child { border-radius: 0 var(--r-pill) var(--r-pill) 0; padding-right: 16px; }\n.seg button:hover { background: var(--blue-soft); color: var(--blue); }\n.seg button.on { background: var(--blue); border-color: var(--blue); color: var(--white); position: relative; z-index: 1; }\n\n/* pastilles de filtres actifs */\n.pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }\n.pill {\n display: inline-flex; align-items: center; gap: 7px;\n border: 1px solid rgba(28, 92, 65, 0.3); background: var(--blue-soft); color: var(--blue);\n border-radius: var(--r-pill); padding: 6px 14px; font-size: 12.5px; font-weight: 600;\n cursor: pointer; transition: all 0.13s ease;\n}\n.pill:hover { background: var(--blue); color: var(--white); }\n.pill-x { font-size: 10px; opacity: 0.65; }\n.pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }\n.pill-clear:hover { background: var(--danger); color: #fff; }\n.chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 4px 4px; flex: none; }\n.link-btn {\n background: none; border: none; padding: 0; color: var(--blue); font: inherit;\n text-decoration: underline; cursor: pointer;\n}\n.field { display: flex; flex-direction: column; gap: 5px; }\n.field label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); padding-left: 2px;\n}\n.field input, .field select {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 11px 14px; font-size: 15px; color: var(--ink); outline: none; font-family: inherit;\n transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; min-height: 44px;\n appearance: none; -webkit-appearance: none;\n}\n.field select {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px;\n}\n.field input:focus, .field select:focus {\n border-color: var(--blue); background: var(--white);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n/* .btn / .btn-primary / .btn-ghost : fournis par tokens.css (ka-ui). */\n\n/* ================= Chips ================= */\n.chips { display: flex; gap: 8px; margin: 16px 0 4px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }\n.chips::-webkit-scrollbar { display: none; }\n.chip {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink);\n border-radius: var(--r-pill); padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer;\n font-family: var(--font-display); white-space: nowrap; min-height: 40px;\n transition: transform 0.13s, box-shadow 0.13s, background 0.13s, color 0.13s;\n}\n.chip:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.chip.on { background: var(--ink); border-color: var(--ink); color: var(--accent); box-shadow: 3px 3px 0 var(--accent); }\n\n/* ================= Results ================= */\n.results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }\n.results-head h2 { font-size: 22px; letter-spacing: -0.02em; color: var(--navy); }\n.results-head span { color: var(--ink-3); font-size: 12.5px; font-weight: 500; }\n.grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));\n gap: 22px; padding-bottom: 70px;\n}\n@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n\n.card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-flat);\n transition: transform 0.16s ease, box-shadow 0.16s ease;\n}\n.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-off); }\n.card:focus-visible { outline: 3px solid rgba(28, 92, 65, 0.4); outline-offset: 2px; }\n.card-img { position: relative; aspect-ratio: 16/10.5; background: var(--grey); overflow: hidden; }\n.card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }\n.card:hover .card-img img { transform: scale(1.05); }\n.card-img .noimg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--ink-3); font-size: 34px; }\n.badge {\n position: absolute; top: 12px; left: 12px; border-radius: var(--r-pill); padding: 4px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: 0.04em;\n background: rgba(255, 255, 255, 0.94); color: var(--navy);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.badge.type { background: var(--orange); color: var(--white); }\n.badge.right { left: auto; right: 12px; background: rgba(255, 255, 255, 0.92); }\n.card-body { padding: 16px 18px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\n.card-price { font-family: var(--font-display); font-weight: 700; font-size: 21px; letter-spacing: -0.02em; color: var(--navy); }\n.card-price small { font-weight: 500; color: var(--ink-3); font-size: 11.5px; }\n.card-title { font-weight: 600; font-size: 14.5px; color: var(--ink); }\n.card-meta { color: var(--ink-3); font-size: 12.5px; display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }\n.card-meta .sep { width: 4px; height: 4px; background: var(--orange); border-radius: 50%; }\n.card-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.source-tag {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--blue); background: var(--blue-soft);\n border-radius: var(--r-pill); padding: 3px 10px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;\n}\n.avail { font-size: 11.5px; color: var(--ink-2); font-weight: 500; text-align: right; }\n\n/* ================= Skeletons ================= */\n@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }\n.skel { border-radius: var(--r-card); border: 1.5px solid var(--ink); overflow: hidden; background: var(--surface); }\n.skel .sk-img, .skel .sk-line {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.skel .sk-img { aspect-ratio: 16/10.5; }\n.skel .sk-line { height: 14px; border-radius: 4px; margin: 12px 16px; }\n.skel .sk-line.short { width: 45%; }\n\n/* ================= Empty / error ================= */\n.notice { text-align: center; padding: 72px 24px; color: var(--ink-2); }\n.notice .big { font-size: 44px; margin-bottom: 10px; }\n.notice h2 { color: var(--navy); }\n\n/* ================= Detail page ================= */\n.detail { padding: 30px 0 90px; }\n.crumbs {\n font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;\n color: var(--ink-3); margin-bottom: 20px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;\n}\n.crumbs a { border-bottom: 1.5px solid transparent; }\n.crumbs a:hover { color: var(--blue); border-color: var(--blue); }\n.detail-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n/* les enfants de grille ont min-width:auto par défaut : un contenu large\n (grille du quartier) déborderait de l'écran mobile sans ceci */\n.detail-grid > * { min-width: 0; }\n\n.gallery { display: flex; flex-direction: column; gap: 10px; }\n.gallery-main {\n border-radius: var(--r-card); overflow: hidden; border: 1.5px solid var(--ink);\n aspect-ratio: 16/10; background: var(--grey); cursor: zoom-in; box-shadow: var(--shadow-off-soft);\n}\n.gallery-main img { width: 100%; height: 100%; object-fit: cover; }\n.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; }\n.thumbs button {\n border: 2px solid var(--line); border-radius: 12px; overflow: hidden; padding: 0; cursor: pointer;\n aspect-ratio: 4/3; background: var(--grey); transition: border-color 0.12s ease, transform 0.12s ease;\n}\n.thumbs button:hover { transform: translateY(-2px); }\n.thumbs button.on { border-color: var(--orange); }\n.thumbs img { width: 100%; height: 100%; object-fit: cover; }\n\n.panel {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px; position: sticky; top: 120px;\n}\n@media (max-width: 900px) { .panel { position: static; } }\n.panel .price { font-family: var(--font-display); font-size: 34px; font-weight: 700; letter-spacing: -0.03em; color: var(--navy); }\n.panel .price small { font-size: 12.5px; color: var(--ink-3); font-weight: 500; }\n.panel h1 { font-size: 22px; margin: 8px 0 2px; }\n.panel .loc { color: var(--ink-2); font-size: 14px; }\n.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n/* .klabel : fourni par tokens.css (ka-ui). */\n.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n.amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }\n.amenity {\n background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 600;\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-pill); padding: 5px 13px;\n}\n.cta {\n display: block; text-align: center; background: var(--orange); color: var(--white);\n font-weight: 600; font-family: var(--font-display); border-radius: var(--r-pill);\n padding: 15px; min-height: 48px;\n box-shadow: 0 8px 22px rgba(255, 106, 0, 0.34); transition: all 0.14s ease;\n}\n.cta:hover { background: var(--orange-deep); }\n.cta:active { transform: translateY(1px); }\n.panel .fine { font-size: 11.5px; color: var(--ink-3); margin-top: 14px; text-align: center; }\n\n/* ================= Lightbox ================= */\n.lightbox {\n position: fixed; inset: 0; background: rgba(20, 24, 20, 0.94); z-index: var(--z-modal, 900);\n display: flex; align-items: center; justify-content: center; cursor: zoom-out;\n padding: max(16px, env(safe-area-inset-top)) 16px;\n}\n.lightbox img { max-width: 94vw; max-height: 90vh; border-radius: 12px; }\n\n/* Lightbox v2 : balayage entre photos (scroll-snap) + pincement pour zoomer */\n.lightbox-v2 { padding: 0; cursor: default; display: block; }\n.lightbox-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%;\n -webkit-overflow-scrolling: touch; scrollbar-width: none;\n touch-action: pan-x pinch-zoom;\n}\n.lightbox-track::-webkit-scrollbar { display: none; }\n.lightbox-cell {\n flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center;\n display: flex; align-items: center; justify-content: center;\n padding: max(52px, env(safe-area-inset-top)) 10px max(24px, env(safe-area-inset-bottom));\n overflow: hidden;\n}\n.lightbox-cell img {\n max-width: 100%; max-height: 100%; border-radius: 10px;\n user-select: none; -webkit-user-drag: none; will-change: transform;\n}\n.lightbox-close {\n position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.4);\n background: rgba(20, 24, 20, 0.6); color: #fff; font-size: 18px; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.lightbox-count {\n position: fixed; left: 50%; transform: translateX(-50%); right: auto;\n top: max(22px, env(safe-area-inset-top)); bottom: auto; z-index: 3;\n}\n.lightbox-v2 .carousel-nav { position: fixed; z-index: 3; }\n\n/* visuel de secours (SmartImg) directement sous .carousel (galerie vide) */\n.carousel > img { display: block; width: 100%; aspect-ratio: 16/11; object-fit: cover; }\n\n/* ================= Sources page ================= */\n.sources { padding: 44px 0 90px; }\n.sources h1 { font-size: clamp(28px, 4vw, 40px); margin: 10px 0 6px; color: var(--navy); }\n.sources .sub { color: var(--ink-2); margin-bottom: 30px; max-width: 700px; }\n.src-wrap { overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); background: var(--surface); }\n.src-table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 720px; }\n.src-table th {\n text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: 0.1em; color: var(--ink-3); padding: 14px 18px;\n border-bottom: 1px solid var(--line-strong); background: var(--grey);\n}\n.src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }\n.src-table tr:last-child td { border-bottom: none; }\n.src-table tr:hover td { background: var(--navy-soft); }\n.src-table a { color: var(--blue); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.pill { display: inline-block; border-radius: var(--r-pill); padding: 3px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }\n.pill.ok { background: var(--blue-soft); color: var(--blue); }\n.pill.todo { background: var(--orange-soft); color: var(--orange-deep); }\n.count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--navy); }\n\n/* ================= Page Statistiques ================= */\n.stats-page { padding: 44px 0 90px; }\n.stats-title { font-size: clamp(30px, 4.6vw, 46px); margin: 10px 0 6px; color: var(--navy); }\n.stats-page .sub { color: var(--ink-2); max-width: 640px; margin-bottom: 30px; }\n\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 30px; }\n.tile {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n padding: 18px 20px; box-shadow: var(--shadow-flat);\n}\n.tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--navy); }\n.tile-k { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }\n.hero-tile { background: var(--navy); color: var(--white); border-color: var(--navy); }\n.hero-tile .tile-v { color: var(--white); }\n.hero-tile .tile-k { color: rgba(255, 255, 255, 0.6); }\n\n.viz-card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px 28px 20px; margin-bottom: 22px;\n}\n.viz-card h2 { font-size: 19px; letter-spacing: -0.01em; color: var(--navy); }\n.viz-sub { font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 4px 0 20px; }\n.viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n\n/* Histogramme (mono-série, bleu vif) */\n.histo { display: flex; align-items: stretch; gap: 2px; height: 240px; }\n.histo-col { flex: 1; display: flex; flex-direction: column; min-width: 0; cursor: default; }\n.histo-bar-zone { flex: 1; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line-strong); }\n.histo-bar {\n width: 100%; background: var(--blue); border-radius: 6px 6px 0 0;\n min-height: 2px; transition: background 0.12s ease;\n}\n.histo-col:hover .histo-bar { background: var(--orange); }\n.histo-x { font-size: 9.5px; color: var(--ink-3); text-align: left; height: 18px; padding-top: 5px; overflow: visible; white-space: nowrap; }\n\n/* Barres horizontales (mono-série) */\n.hbars { display: flex; flex-direction: column; gap: 7px; }\n.hbar-row {\n display: grid; grid-template-columns: minmax(96px, 170px) 1fr auto;\n gap: 12px; align-items: center; min-height: 26px; cursor: default;\n}\n.hbar-label { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.hbar-label a { border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.hbar-label a:hover { color: var(--blue); }\n.hbar-track { background: var(--grey); border-radius: var(--r-pill); height: 18px; overflow: hidden; }\n.hbar-fill {\n display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill);\n min-width: 2px; transition: background 0.12s ease;\n}\n.hbar-row:hover .hbar-fill { background: var(--orange); }\n.hbar-value { font-size: 11.5px; font-weight: 700; white-space: nowrap; color: var(--navy); }\n.hbar-value em { font-style: normal; font-weight: 500; color: var(--ink-3); }\n@media (max-width: 640px) {\n .hbar-row { grid-template-columns: minmax(80px, 110px) 1fr auto; gap: 8px; }\n .hbar-value em { display: none; }\n .viz-card { padding: 20px 16px 14px; }\n .histo { height: 170px; }\n}\n\n/* Infobulle */\n.viz-tip {\n position: fixed; z-index: var(--z-toast, 950); pointer-events: none; max-width: 180px;\n background: var(--navy); color: var(--white); border-radius: 12px;\n padding: 9px 12px; font-size: 12px; line-height: 1.5;\n box-shadow: 0 8px 24px rgba(20, 24, 20, 0.35);\n}\n.viz-tip-title { font-family: var(--font-display); font-weight: 700; color: var(--orange); margin-bottom: 2px; }\n\n/* Vue tableau repliable (accessibilité / relief) */\n.viz-table { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 10px; }\n.viz-table summary {\n cursor: pointer; font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em; color: var(--blue);\n}\n.viz-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }\n.viz-table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line-strong); }\n.viz-table td { padding: 6px 10px; border-bottom: 1px solid var(--line); }\n\n.stats-foot { font-size: 11.5px; color: var(--ink-3); letter-spacing: 0.02em; margin-top: 6px; }\n\n/* ================= Mobile : feuille de filtres + FAB ================= */\n.sheet-head { display: none; }\n.sheet-apply { display: none; }\n.sheet-backdrop { display: none; }\n.fab { display: none; }\n\n@media (max-width: 640px) {\n /* En-tête compact */\n .header-inner { height: 56px; }\n .brand { font-size: 21px; }\n .nav a { padding: 8px 13px; font-size: 13.5px; }\n .ticker { font-size: 10.5px; padding: 6px 0; }\n\n /* Héro resserré + stats en rangée défilante */\n .hero h1 { font-size: clamp(30px, 8.6vw, 42px); }\n .hero p.lede { font-size: 15px; }\n .stat-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 6px; margin-right: -16px; padding-right: 16px; }\n .stat-row::-webkit-scrollbar { display: none; }\n .stat-chip { flex: 0 0 auto; white-space: nowrap; }\n\n /* La barre de filtres devient une feuille coulissante (bottom sheet) */\n .filterbar { display: none; }\n .filterbar.open .f-primary { flex-direction: column; }\n .filterbar.open .f-search { align-self: stretch; max-width: none; min-height: 46px; }\n .filterbar.open .f-search input { font-size: 16px; } /* anti-zoom iOS */\n .filterbar.open .f-ctl select { max-width: none; width: 100%; }\n .filterbar.open .f-more { display: none; }\n .filterbar.open .f-adv { margin-top: 4px; }\n .filterbar.open {\n display: flex; flex-direction: column; gap: 12px;\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);\n margin: 0; border-radius: 24px 24px 0 0; border: none;\n max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;\n padding: 16px 18px calc(18px + env(safe-area-inset-bottom));\n box-shadow: 0 -16px 48px rgba(20, 24, 20, 0.28);\n animation: sheet-up 0.22s ease;\n }\n @keyframes sheet-up { from { transform: translateY(30%); opacity: 0.4; } to { transform: none; opacity: 1; } }\n .filterbar.open .sheet-head {\n display: flex; justify-content: space-between; align-items: center;\n font-family: var(--font-display); font-weight: 700; font-size: 17px;\n letter-spacing: -0.01em; color: var(--navy);\n position: sticky; top: -16px; background: var(--surface); padding: 6px 0 8px;\n border-bottom: 1px solid var(--line); margin-bottom: 2px; z-index: 1;\n }\n .sheet-close {\n border: 1.5px solid var(--ink); background: var(--surface); border-radius: 50%;\n width: 36px; height: 36px; font-size: 15px; cursor: pointer; line-height: 1; color: var(--navy);\n }\n .filterbar.open .sheet-apply { display: block; width: 100%; }\n .sheet-backdrop {\n display: block; position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.45); backdrop-filter: blur(2px);\n }\n\n /* Bouton flottant */\n .fab {\n display: flex; align-items: center; gap: 6px;\n position: fixed; left: 50%; transform: translateX(-50%);\n bottom: calc(74px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: 80;\n background: var(--orange); color: var(--white); border: none;\n border-radius: var(--r-pill); padding: 13px 24px; font-family: var(--font-display);\n font-weight: 600; font-size: 15px; cursor: pointer;\n box-shadow: 0 10px 28px rgba(255, 106, 0, 0.4);\n }\n .fab:active { transform: translateX(-50%) scale(0.97); }\n\n /* Fiche logement : vignettes en bande défilante + panneau non collant */\n .thumbs { display: flex; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }\n .thumbs::-webkit-scrollbar { display: none; }\n .thumbs button { flex: 0 0 96px; }\n .detail { padding-top: 20px; }\n .panel { padding: 20px; }\n .panel .price { font-size: 28px; }\n .results-head { margin-top: 20px; }\n .chips { margin-right: -16px; padding-right: 16px; }\n .notice { padding: 48px 16px; }\n}\n\n/* ================= Barre de navigation inférieure (mobile) ================= */\n.tabbar { display: none; }\n@media (max-width: 760px) {\n .tabbar {\n display: grid; grid-template-columns: repeat(4, 1fr);\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border-top: 1px solid var(--line);\n padding: 6px 4px calc(6px + env(safe-area-inset-bottom));\n }\n .tabbar a {\n display: flex; flex-direction: column; align-items: center; gap: 3px;\n padding: 5px 2px; min-height: 48px; justify-content: center;\n font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n border-radius: var(--r-ctl); transition: color 0.12s ease;\n }\n .tabbar a.active { color: var(--orange); }\n .tabbar a svg { display: block; }\n body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }\n .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }\n}\n\n/* Anti-zoom iOS : les champs doivent faire >= 16px\n (filet global : tokens.css force aussi 16 px au doigt) */\n@media (max-width: 900px) {\n .field input, .field select,\n .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }\n}\n\n/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */\n@media (pointer: coarse) {\n .brand { position: relative; }\n .brand::after { content: \"\"; position: absolute; inset: -8px; }\n}\n\n/* ================= Footer ================= */\n.footer { background: var(--navy); color: rgba(255, 255, 255, 0.72); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; }\n.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 32px; color: var(--white); letter-spacing: -0.03em; margin-bottom: 4px; display: flex; align-items: center; }\n.footer .fbrand .logo-ico { margin-right: 13px; }\n.footer .fbrand .ka { color: var(--orange); }\n.footer .fbaseline { color: rgba(255, 255, 255, 0.65); font-size: 14px; margin-bottom: 16px; }\n.footer .fbaseline .hl { color: var(--orange); font-weight: 600; }\n.footer b { color: var(--white); }\n.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }\n.footer .fmono { font-size: 11.5px; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.42); margin-top: 18px; }\n\n/* --- Carte interactive (mission 4) --------------------------------------- */\n.results-tools { display: flex; align-items: center; gap: 14px; }\n.view-toggle {\n display: inline-flex; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); overflow: hidden; background: var(--surface);\n box-shadow: var(--shadow-flat);\n}\n.view-toggle button {\n border: 0; background: transparent; padding: 7px 16px; cursor: pointer;\n font-size: 12.5px; font-weight: 600; color: var(--ink-2);\n}\n/* zone tactile >= 44 px au doigt (coarse pointer = mobile/tablette) */\n@media (pointer: coarse) {\n .view-toggle button { min-height: 44px; padding: 10px 18px; }\n}\n.view-toggle button + button { border-left: 1px solid var(--line); }\n.view-toggle button.on { background: var(--blue); color: var(--white); }\n\n.map-split {\n display: grid; grid-template-columns: minmax(300px, 400px) 1fr; gap: 18px;\n height: calc(100dvh - 170px); min-height: 420px;\n}\n.map-list {\n overflow-y: auto; display: flex; flex-direction: column; gap: 14px;\n padding-right: 4px; scrollbar-width: thin;\n}\n.map-list .card { margin: 0; flex: 0 0 auto; }\n.mapview {\n width: 100%; height: 100%; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.map-loading {\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-3); font-size: 13px;\n}\n\n/* mobile d'abord : carte plein écran, liste masquée (bascule via l'onglet) */\n@media (max-width: 780px) {\n .map-split { grid-template-columns: 1fr; height: calc(100dvh - 210px); }\n .map-list { display: none; }\n .results-tools { width: 100%; justify-content: space-between; }\n}\n\n/* popup MapLibre au style Lou-Ka */\n.maplibregl-popup-content {\n padding: 0; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); box-shadow: var(--shadow-off);\n font-family: var(--font-body);\n}\n.maplibregl-popup-close-button {\n font-size: 18px; padding: 2px 8px; color: var(--white); z-index: 2;\n text-shadow: 0 0 4px rgba(20, 24, 20, 0.8);\n}\n.mv-pop img, .mv-noimg { width: 100%; height: 130px; object-fit: cover; }\n.mv-noimg { display: flex; align-items: center; justify-content: center;\n font-size: 34px; background: var(--grey); }\n.mv-pop-body { padding: 10px 12px 12px; }\n.mv-pop-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--navy); }\n.mv-pop-price small { font-weight: 400; color: var(--ink-3); font-size: 12px; }\n.mv-pop-title { font-size: 13px; color: var(--ink-2); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mv-pop-meta { font-size: 11.5px; color: var(--ink-2); margin-top: 6px; }\n.mv-pop-src { font-size: 10px; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }\n.mv-pop-cta {\n display: block; margin-top: 10px; padding: 9px 10px; text-align: center;\n background: var(--orange); color: var(--white); border-radius: var(--r-pill);\n font-weight: 600; font-size: 13px;\n}\n.mv-pop-cta:hover { background: var(--orange-deep); }\n\n/* --- Commodités de proximité (fiche) -------------------------------------- */\n.poi-list {\n list-style: none; margin: 0; padding: 0;\n display: flex; flex-direction: column; gap: 2px;\n}\n.poi-list li {\n display: flex; align-items: center; gap: 9px;\n padding: 5px 2px; border-bottom: 1px solid var(--line);\n font-size: 13px;\n}\n.poi-list li:last-child { border-bottom: 0; }\n.poi-ico { width: 20px; text-align: center; flex: 0 0 auto; }\n.poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.poi-dist { font-size: 11.5px; color: var(--navy); font-weight: 600; flex: 0 0 auto; }\n\n/* --- Menu déroulant mobile ------------------------------------------------- */\n.menu-btn {\n display: none; position: relative; z-index: 60;\n width: 44px; height: 44px; margin-left: auto;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); cursor: pointer; padding: 0;\n flex-direction: column; align-items: center; justify-content: center; gap: 5px;\n box-shadow: var(--shadow-flat);\n transition: box-shadow 0.15s ease, transform 0.15s ease;\n}\n.menu-btn:active { transform: scale(0.96); }\n.menu-btn span {\n display: block; width: 18px; height: 2px; background: var(--navy);\n border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease;\n}\n.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }\n.menu-btn.open span:nth-child(2) { opacity: 0; }\n.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }\n\n.mobile-menu {\n display: none; position: absolute; left: 0; right: 0; top: 100%;\n background: var(--white); border-bottom: 1px solid var(--line);\n box-shadow: 0 18px 30px rgba(20, 24, 20, 0.14);\n padding: 8px 16px calc(16px + env(safe-area-inset-bottom));\n max-height: 0; overflow: hidden; opacity: 0;\n transition: max-height 0.3s ease, opacity 0.22s ease;\n}\n.mobile-menu { z-index: var(--z-modal, 900); }\n.mobile-menu.open { max-height: 480px; opacity: 1; }\n/* Le drawer vit DANS le header : quand il est ouvert, le header monte au niveau\n modal pour peindre au-dessus du .mm-backdrop (frère du header, overlay 800). */\n.header:has(.mobile-menu.open) { z-index: var(--z-modal, 900); }\n.mm-link {\n display: flex; align-items: center; gap: 12px;\n padding: 15px 10px; min-height: 52px;\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n letter-spacing: -0.02em; border-bottom: 1px solid var(--line);\n color: var(--navy);\n opacity: 0; transform: translateY(-8px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); }\n.mm-link.active { color: var(--orange); }\n.mm-link.active .mm-arrow { opacity: 1; color: var(--orange); }\n.mm-ico { width: 26px; text-align: center; font-size: 17px; }\n.mm-arrow { margin-left: auto; opacity: 0.25; color: var(--orange); transition: opacity 0.15s ease; }\n.mm-link:active { background: var(--orange-soft); border-radius: var(--r-ctl); }\n.mm-foot {\n padding: 12px 10px 4px;\n font-size: 11px; color: var(--ink-3); letter-spacing: 0.02em;\n}\n.mm-backdrop {\n position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px);\n}\n@media (max-width: 760px) {\n .menu-btn { display: flex; }\n .nav { display: none; }\n .mobile-menu { display: block; }\n}\n\n/* --- Bandeau de consentement (témoins) ------------------------------------- */\n.cookie-banner {\n position: fixed; left: 12px; right: 12px;\n bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80;\n animation: cookie-in 0.35s ease;\n}\n@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } }\n.cookie-inner {\n max-width: 720px; margin: 0 auto; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 16px 18px;\n}\n.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }\n.cookie-text b { color: var(--navy); }\n.cookie-text a { text-decoration: underline; text-underline-offset: 2px; }\n.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n.cookie-opt {\n display: flex; align-items: flex-start; gap: 10px; font-size: 13px;\n color: var(--ink-2); cursor: pointer;\n}\n.cookie-opt input { margin-top: 2px; accent-color: var(--orange); width: 16px; height: 16px; }\n.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; }\n@media (max-width: 640px) {\n .cookie-actions { justify-content: stretch; }\n .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; }\n}\n\n/* --- Page confidentialité + lien pied de page ------------------------------- */\n.legal { padding: 48px 0 70px; max-width: 760px; }\n.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; color: var(--navy); }\n.legal section { margin-top: 26px; }\n.legal h3 { font-size: 18px; margin-bottom: 8px; }\n.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }\n.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }\n.flink {\n border: 0; background: none; padding: 0; cursor: pointer;\n color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px;\n}\n\n/* --- Section « Le quartier » (fiche) --------------------------------------- */\n.quartier { margin-top: 34px; }\n.quartier h2 { font-size: 24px; letter-spacing: -0.02em; color: var(--navy); }\n.q-sub { color: var(--ink-3); font-size: 13px; margin: 4px 0 16px; }\n.q-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;\n}\n@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n.q-cell {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 12px 14px; box-shadow: var(--shadow-flat);\n}\n.q-val { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.q-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px; }\n.q-prox { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }\n.q-bar { display: flex; align-items: center; gap: 10px; font-size: 13px; }\n.q-bar-label { flex: 0 0 150px; color: var(--ink-2); }\n@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n.q-bar-track {\n flex: 1; height: 10px; background: var(--grey);\n border-radius: var(--r-pill); overflow: hidden;\n}\n.q-bar-fill { display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill); }\n.q-bar-num { flex: 0 0 30px; text-align: right;\n font-size: 11.5px; font-weight: 700; color: var(--navy); }\n.q-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.q-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 7px 14px; font-size: 12.5px; background: var(--surface);\n}\n.q-badge.cool { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.q-badge.hot { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.quartier .fine { margin-top: 10px; }\n\n/* --- Fiche v2 (mobile-first) ------------------------------------------------ */\n/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka\n « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`\n / `display:contents` pour réordonner — les blocs sans `order` retombaient à\n order:0 et passaient devant la galerie sur mobile. Mobile : colonnes\n empilées ; desktop : grid 2 colonnes (logement | synthèse). */\n.fiche { display: flex; flex-direction: column; gap: 22px; }\n.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n@media (min-width: 900px) {\n .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n .f-col { gap: 26px; }\n}\n.f-bloc { min-width: 0; }\n.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n.f-bloc:empty { display: none; }\n\n/* galerie à balayage natif */\n.carousel { position: relative; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); background: var(--grey); box-shadow: var(--shadow-off-soft); }\n.carousel-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch; scrollbar-width: none; aspect-ratio: 16/11;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track img {\n flex: 0 0 100%; width: 100%; object-fit: cover; scroll-snap-align: center;\n cursor: zoom-in;\n}\n.carousel-empty { display: flex; align-items: center; justify-content: center;\n aspect-ratio: 16/11; font-size: 48px; }\n.carousel-count {\n position: absolute; right: 12px; bottom: 12px; z-index: 2;\n background: rgba(20, 24, 20, 0.78); color: var(--white);\n font-size: 12px; font-weight: 600;\n padding: 4px 11px; border-radius: var(--r-pill);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.carousel-nav {\n position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--ink);\n background: rgba(255, 255, 255, 0.92); color: var(--navy); font-size: 22px; cursor: pointer;\n display: flex; align-items: center; justify-content: center; line-height: 1;\n box-shadow: var(--shadow-flat);\n}\n.carousel-nav.prev { left: 10px; } .carousel-nav.next { right: 10px; }\n@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n\n/* bandeau prix + badge marché */\n.f-hero .price { font-size: clamp(34px, 8vw, 44px); }\n.deal-badge {\n display: inline-flex; align-items: center; gap: 6px; margin: 8px 0 4px;\n border-radius: var(--r-pill); padding: 7px 14px; font-size: 13px; font-weight: 600;\n border: 1.5px solid var(--ink);\n}\n.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.deal-ok { background: var(--surface); color: var(--ink-2); }\n.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.f-hero h1 { font-size: clamp(20px, 4.5vw, 26px); margin-top: 8px; }\n\n/* chips clés à défilement horizontal */\n.chips-scroll {\n display: flex; gap: 8px; overflow-x: auto; padding: 12px 0 6px;\n scrollbar-width: none; -webkit-overflow-scrolling: touch;\n}\n.chips-scroll::-webkit-scrollbar { display: none; }\n.chip-key {\n flex: 0 0 auto; background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 8px 15px;\n font-size: 12px; font-weight: 600; white-space: nowrap; min-height: 34px;\n display: inline-flex; align-items: center;\n}\n\n/* ancres de navigation rapide */\n.ancres {\n display: flex; gap: 4px; overflow-x: auto; padding: 6px 0 2px;\n scrollbar-width: none; border-bottom: 1px solid var(--line); margin-bottom: 8px;\n}\n.ancres::-webkit-scrollbar { display: none; }\n.ancres a {\n flex: 0 0 auto; padding: 9px 14px; font-size: 13.5px; font-weight: 600;\n color: var(--ink-2); border-radius: var(--r-pill); min-height: 44px;\n display: inline-flex; align-items: center;\n}\n.ancres a:hover { background: var(--orange-soft); color: var(--orange); }\nhtml { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */\n\n/* description restructurée */\n.enbref {\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-card); padding: 12px 16px; color: var(--navy);\n font-size: 15px; margin: 0 0 14px;\n}\n.desc-section { margin-bottom: 12px; }\n.desc-section h4 { font-size: 15px; margin-bottom: 3px; }\n.desc-section p { color: var(--ink-2); font-size: 14.5px; margin: 0; white-space: pre-line; }\n.texte-original { margin-top: 12px; }\n.texte-original summary {\n cursor: pointer; font-size: 12.5px;\n color: var(--ink-3); min-height: 44px; display: flex; align-items: center;\n}\n.texte-original p { color: var(--ink-3); font-size: 13px; white-space: pre-line; }\n\n/* inclusions : confirmées vs mentionnées */\n.amenity.confirmed { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.25); color: var(--blue); font-weight: 600; }\n.amenity.unconfirmed { background: transparent; border-style: dashed; border-color: var(--line-strong); color: var(--ink-3); }\n\n/* détails pratiques : historique de prix */\n.prix-histo {\n margin-top: 12px; padding: 10px 14px; border-radius: var(--r-card);\n border: 1.5px solid var(--ink); font-size: 13.5px; background: var(--surface);\n}\n.prix-histo.down { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n\n/* quartier compacté sur mobile */\n@media (max-width: 640px) {\n .q-cell { padding: 9px 11px; }\n .q-val { font-size: 16px; }\n .q-bar { gap: 8px; font-size: 12px; }\n .q-bar-track { height: 8px; }\n}\n.q-badge-sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }\n\n/* POI groupés repliables */\n.poi-groupe { border: 1.5px solid var(--ink); border-radius: var(--r-card);\n margin-bottom: 8px; background: var(--surface); overflow: hidden; }\n.poi-groupe summary {\n display: flex; justify-content: space-between; align-items: center; gap: 10px;\n padding: 12px 14px; cursor: pointer; font-weight: 600; font-size: 14px;\n min-height: 48px; list-style: none;\n}\n.poi-groupe summary::-webkit-details-marker { display: none; }\n.poi-groupe summary::after { content: \"▾\"; color: var(--ink-3); transition: transform 0.15s ease; }\n.poi-groupe[open] summary::after { transform: rotate(180deg); }\n.poi-resume { font-size: 11.5px; color: var(--ink-3); font-weight: 400; }\n.poi-groupe .poi-list { padding: 0 14px 10px; }\n\n/* CTA sticky mobile */\n.cta-sticky {\n position: fixed; left: 0; right: 0; z-index: var(--z-dropdown, 700);\n bottom: var(--consent-h, 0px);\n display: flex; align-items: center; gap: 12px;\n background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(12px);\n border-top: 1px solid var(--line);\n padding: 10px 16px calc(10px + env(safe-area-inset-bottom));\n}\n.cta-sticky .cta { flex: 1; margin: 0; text-align: center; }\n.cta-sticky-prix { font-family: var(--font-display); font-weight: 700; font-size: 19px; white-space: nowrap; color: var(--navy); }\n.cta-sticky-prix small { font-weight: 500; font-size: 10px; color: var(--ink-3); }\n@media (min-width: 900px) { .cta-sticky { display: none; } }\n@media (max-width: 899px) {\n .cta-desktop { display: none; }\n .detail { padding-bottom: 84px; } /* place pour le CTA sticky */\n}\n.f-foot { margin-top: 26px; }\n\n/* --- Passerelle vers l'annonce originale ------------------------------------ */\n.passerelle {\n min-height: calc(100dvh - 120px); display: flex; align-items: center;\n justify-content: center; padding: 24px 16px;\n}\n.pass-card {\n width: 100%; max-width: 520px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 30px 28px; text-align: center;\n}\n.pass-brands {\n display: flex; align-items: center; justify-content: center; gap: 14px;\n margin-bottom: 22px; flex-wrap: wrap;\n}\n.pass-brand {\n font-family: var(--font-display); font-weight: 700; font-size: 24px;\n letter-spacing: -0.03em; display: inline-flex; align-items: center; color: var(--navy);\n}\n.pass-brand .ka { color: var(--orange); }\n.pass-trajet { display: inline-flex; align-items: center; gap: 5px; }\n.pass-dot {\n width: 6px; height: 6px; border-radius: 50%; background: var(--orange);\n opacity: 0.25; animation: pass-dot 1.2s ease infinite;\n}\n.pass-dot:nth-child(2) { animation-delay: 0.2s; }\n.pass-dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes pass-dot { 40% { opacity: 1; transform: translateX(2px); } }\n.pass-fleche { color: var(--orange); font-size: 18px; }\n.pass-gestionnaire {\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n display: inline-flex; align-items: center; gap: 8px;\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-pill); padding: 6px 16px; color: var(--blue);\n}\n.pass-card h1 { font-size: 20px; letter-spacing: -0.02em; }\n.pass-texte { color: var(--ink-2); font-size: 14.5px; margin: 10px 0 20px; }\n.pass-progress {\n height: 10px; border-radius: var(--r-pill);\n overflow: hidden; background: var(--grey); margin-bottom: 20px;\n}\n.pass-progress-fill {\n display: block; height: 100%; width: 0; background: var(--orange);\n border-radius: var(--r-pill);\n}\n.pass-progress-fill.go { animation: pass-fill 2.2s linear forwards; }\n@keyframes pass-fill { to { width: 100%; } }\n@media (prefers-reduced-motion: reduce) {\n .pass-progress-fill.go { animation: none; width: 100%; }\n .pass-dot { animation: none; opacity: 0.7; }\n}\n.pass-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; }\n\n/* --- boutons PDF -------------------------------------------------------- */\n.btn-pdf { display: block; text-align: center; margin-top: 10px; width: 100%; }\n\n/* --- Onglets de la page Stats -------------------------------------------- */\n.onglets {\n display: flex; gap: 8px; overflow-x: auto; margin: 26px 0 22px;\n padding-bottom: 4px; scrollbar-width: none;\n border-bottom: 1px solid var(--line);\n}\n.onglets::-webkit-scrollbar { display: none; }\n.onglet {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;\n border: 1.5px solid var(--ink);\n border-radius: var(--r-pill);\n background: var(--surface); color: var(--ink-2); cursor: pointer;\n padding: 10px 18px; font-weight: 600; font-size: 13.5px; min-height: 44px;\n margin-bottom: 8px;\n transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;\n}\n.onglet:hover { background: var(--blue-soft); color: var(--blue); border-color: var(--blue); }\n.onglet.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* baisses de prix */\n.baisses { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }\n.baisses li {\n display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n padding: 9px 4px; border-bottom: 1px solid var(--line); font-size: 13.5px;\n}\n.baisses li a { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }\n.baisse-ville { color: var(--ink-3); font-size: 11.5px; }\n.baisse-prix { margin-left: auto; }\n.baisse-prix s { color: var(--ink-3); }\n.baisse-pct {\n font-style: normal; font-weight: 700;\n background: var(--blue-soft);\n color: var(--blue); border-radius: var(--r-pill); padding: 2px 9px;\n font-size: 11px; margin-left: 8px;\n}\n.alertes { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n\n/* ================= Pagination (12 annonces/page) ================= */\n.pager {\n display: flex; align-items: center; justify-content: space-between;\n gap: 16px; flex-wrap: wrap; margin: 26px 0 10px; padding-top: 18px;\n border-top: 1px solid var(--line);\n}\n.pager-info {\n font-size: 12.5px; color: var(--ink-2); letter-spacing: 0.02em;\n}\n.pager-info em { font-style: normal; color: var(--ink-3); }\n.pager-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.pager-btn {\n min-width: 38px; height: 38px; padding: 0 10px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); cursor: pointer;\n font-size: 13px; font-weight: 600;\n color: var(--navy); transition: all 0.12s ease;\n}\n.pager-btn:hover:not(:disabled):not(.on) {\n background: var(--blue-soft); border-color: var(--blue); color: var(--blue);\n}\n.pager-btn.on { background: var(--navy); border-color: var(--navy); color: var(--white); cursor: default; }\n.pager-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n.pager-ellipsis { color: var(--ink-3); padding: 0 2px; }\n.map-list .pager { margin: 10px 0 4px; }\n@media (max-width: 640px) {\n .pager { justify-content: center; }\n .pager-info { width: 100%; text-align: center; order: 2; }\n}\n\n/* ================= Icônes SVG inline — alignement ================= */\n.chip-ico { display: inline-flex; align-items: center; gap: 6px; }\n.chip-ico svg { flex: none; }\n.view-toggle button, .seg button { display: inline-flex; align-items: center; gap: 6px; }\n.fab { align-items: center; gap: 8px; }\n.fab svg { flex: none; }\n.mm-ico { display: inline-flex; align-items: center; }\n.notice .big { display: flex; justify-content: center; color: var(--ink-3); }\n.notice .big svg { display: block; }\n.card-img .noimg { color: var(--ink-3); }\n.badge.right { display: inline-flex; align-items: center; gap: 4px; }\n.btn-pdf { display: inline-flex; align-items: center; gap: 7px; }\n.f-search svg { flex: none; color: var(--ink-3); }\n.mv-noimg { display: flex; align-items: center; justify-content: center; color: var(--ink-3); }\n\n/* footer — liens (Groupe KA, courriels, mentions légales) */\n.footer .frow a { color: var(--white); text-decoration: underline; text-decoration-color: rgba(255, 106, 0, 0.5); text-underline-offset: 3px; }\n.footer .frow a:hover { color: var(--orange); }\n.footer .fmono a:hover { color: var(--orange); }\n\n/* ================= Compte (connexion Google) ================= */\n.login-btn {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 8px 16px; margin-left: 10px; min-height: 38px;\n background: var(--navy); border: none;\n border-radius: var(--r-pill); font-weight: 600; font-size: 13.5px; color: var(--white);\n transition: all 0.12s ease; white-space: nowrap;\n box-shadow: 0 4px 14px rgba(20, 24, 20, 0.22);\n}\n.login-btn:hover { background: var(--blue); }\n.account { position: relative; margin-left: 10px; }\n.account-btn {\n width: 38px; height: 38px; padding: 0; border: 2px solid var(--orange);\n border-radius: var(--r-pill); overflow: hidden; cursor: pointer;\n background: var(--orange-soft); display: flex; align-items: center; justify-content: center;\n}\n.account-btn img { width: 100%; height: 100%; object-fit: cover; }\n.account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }\n/* Click-catcher plein écran rendu via createPortal à la racine de \n (un fixed descendant du header blurré se re-parentait → 66 px de haut).\n z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le\n header et doit peindre au-dessus du voile. */\n.account-backdrop { position: fixed; inset: 0; z-index: 40; }\n.account-menu {\n position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;\n min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off); overflow: hidden;\n}\n.account-id { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-bottom: 1px solid var(--line); }\n.account-id b { font-size: 14px; }\n.account-id span { font-size: 11.5px; color: var(--ink-3); word-break: break-all; }\n.account-menu button {\n width: 100%; text-align: left; padding: 11px 14px; border: 0; cursor: pointer;\n background: transparent; font-weight: 600; font-size: 13.5px; color: var(--danger);\n}\n.account-menu button:hover { background: var(--orange-soft); color: var(--navy); }\n@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n\n/* ================= Profil — carte de membre Groupe KA ================= */\n.profil { padding-top: 40px; padding-bottom: 60px; }\n.profil h1 { font-size: clamp(30px, 5vw, 44px); margin: 10px 0 26px; color: var(--navy); }\n.profil .lede { max-width: 520px; color: var(--ink-2); margin-bottom: 22px; }\n\n.pc {\n position: relative; max-width: 520px; overflow: hidden;\n background: linear-gradient(135deg, #101a40 0%, var(--navy) 55%, #080e24 100%);\n color: var(--white);\n border-radius: 20px;\n padding: 24px 26px 0; box-shadow: var(--shadow-off);\n}\n.pc-watermark {\n position: absolute; right: -18px; top: -34px; pointer-events: none;\n font-family: var(--font-display); font-weight: 700; font-size: 170px;\n letter-spacing: -0.06em; color: rgba(255, 106, 0, 0.07);\n transform: rotate(-8deg); line-height: 1;\n}\n.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }\n.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }\n.pc-ka { color: var(--orange); margin-left: 1px; display: inline-block; }\n.pc-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.5); }\n.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }\n.pc-id-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2em; color: rgba(255, 106, 0, 0.75); }\n.pc-id {\n font-weight: 700; color: var(--orange);\n font-size: clamp(22px, 6vw, 34px); letter-spacing: 0.14em;\n text-shadow: 0 0 24px rgba(255, 106, 0, 0.35);\n}\n.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }\n.pc-holder { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pc-holder-name { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pc-holder-since { font-size: 10.5px; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.06em; }\n.pc-role-badge {\n display: inline-block; width: fit-content; margin: 3px 0 2px;\n background: var(--orange); color: var(--white);\n font-size: 9px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.12em;\n padding: 2px 8px 3px; border-radius: var(--r-pill);\n}\n.pc-avatar { width: 52px; height: 52px; border-radius: var(--r-pill); border: 2px solid var(--orange); flex: none; }\n.pc-strip { display: flex; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(255, 106, 0, 0.1); border-top: 1px solid rgba(255, 106, 0, 0.25); overflow: hidden; }\n.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--orange); opacity: 0.7; }\n.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.35; }\n.pc-strip i:nth-child(3n+1) { height: 9px; }\n.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.9; }\n.pc-copy { margin-top: 16px; }\n.pc-copy.ok { background: var(--blue); color: var(--white); }\n\n.profil-grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 12px; margin-top: 30px; max-width: 760px;\n}\n.pg-item {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 14px 16px;\n display: flex; flex-direction: column; gap: 4px; box-shadow: var(--shadow-flat);\n}\n.pg-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }\n.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }\n.pg-value.mono { color: var(--blue); }\n\n.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }\n.profil-note a { text-decoration: underline; text-underline-offset: 3px; }\n.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }\n.profil-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n\n.account-kaid { font-size: 10.5px; color: var(--blue); letter-spacing: 0.08em; }\n.account-link {\n display: block; width: 100%; text-align: left; padding: 11px 14px;\n font-weight: 600; font-size: 13.5px; color: var(--navy);\n border-bottom: 1px solid var(--line);\n}\n.account-link:hover { background: var(--orange-soft); }\n\n/* profil public — section + lien partageable */\n.profil-public { margin-top: 30px; max-width: 640px; }\n.profil-public h3 { font-size: 18px; margin-bottom: 6px; }\n.profil-public p { color: var(--ink-2); font-size: 13.5px; margin: 0 0 12px; }\n.public-link {\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n margin-top: 14px; padding: 12px 14px; background: var(--blue-soft);\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-card);\n}\n.public-link a { font-size: 13px; color: var(--blue); text-decoration: underline; text-underline-offset: 3px; word-break: break-all; }\n\n/* ================= Bienvenue — choix du rôle ================= */\n.bienvenue { padding-top: 44px; padding-bottom: 70px; }\n.bienvenue h1 { font-size: clamp(30px, 5vw, 46px); margin: 10px 0 14px; color: var(--navy); }\n.bienvenue .lede { max-width: 480px; color: var(--ink-2); margin-bottom: 30px; }\n.role-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; max-width: 780px; }\n.role-card {\n display: flex; flex-direction: column; align-items: flex-start; gap: 10px;\n text-align: left; padding: 24px 24px 20px; cursor: pointer;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-flat);\n font-family: var(--font-body); transition: all 0.15s ease;\n}\n.role-card:hover:not(:disabled) { transform: translateY(-4px); box-shadow: var(--shadow-off); border-color: rgba(255, 106, 0, 0.4); }\n.role-card:disabled { opacity: 0.6; cursor: wait; }\n.role-ico { display: flex; width: 54px; height: 54px; align-items: center; justify-content: center; background: var(--orange-soft); color: var(--orange); border-radius: 16px; }\n.role-title { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.role-desc { color: var(--ink-2); font-size: 13.5px; }\n.role-perks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }\n.role-perks li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }\n.role-perks li::before {\n content: \"✓\"; flex: none; width: 16px; height: 16px; border-radius: 50%;\n background: var(--orange); color: var(--white); font-size: 10px; font-weight: 700;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.role-cta { margin-top: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--orange); }\n\n/* ================= Favoris — cœur sur les cartes ================= */\n.fav-btn {\n position: absolute; right: 10px; bottom: 10px; z-index: 2;\n width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;\n background: rgba(255, 255, 255, 0.94); border: none;\n border-radius: var(--r-pill); cursor: pointer; color: var(--navy);\n box-shadow: var(--shadow-flat);\n transition: transform 0.12s ease, background 0.12s ease, color 0.12s ease;\n}\n.fav-btn:hover { transform: scale(1.12); }\n.fav-btn.on { background: var(--white); color: var(--orange); }\n.fav-btn.on svg { filter: drop-shadow(0 0 3px rgba(255, 106, 0, 0.35)); }\n\n/* ================= Espace gestionnaire ================= */\n.claim-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; max-width: 640px; }\n.claim-row select {\n flex: 1; min-width: 260px; padding: 11px 14px; font-size: 14px;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); font-family: var(--font-body);\n}\n.claim-error { color: var(--danger); font-size: 13.5px; font-weight: 600; margin-top: 10px; }\n.org-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 22px; }\n.org-stat { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); }\n.org-form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }\n.org-form h3 { font-size: 18px; }\n.org-form label { display: flex; flex-direction: column; gap: 5px; flex: 1; }\n.org-form label span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }\n.org-form input, .org-form textarea {\n padding: 10px 14px; font-size: 14px; font-family: var(--font-body);\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);\n}\n.org-form textarea { resize: vertical; }\n.org-form input:focus, .org-form textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.org-form-row { display: flex; gap: 12px; flex-wrap: wrap; }\n.org-form .btn { align-self: flex-start; }\n\n/* ================= Page publique /g/{source} ================= */\n.orgpub-head { display: flex; gap: 20px; align-items: flex-start; margin-top: 8px; }\n.orgpub-logo { width: 84px; height: 84px; object-fit: contain; background: var(--surface); border: 1.5px solid var(--ink); border-radius: 18px; padding: 8px; flex: none; box-shadow: var(--shadow-flat); }\n.orgpub-id h1 { font-size: clamp(26px, 4.4vw, 40px); margin: 6px 0 8px; color: var(--navy); }\n.orgpub-tagline { font-size: 16px; color: var(--ink-2); margin: 0 0 12px; max-width: 640px; }\n.orgpub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.orgpub-meta a.stat-chip:hover { background: var(--blue-soft); }\n.orgpub-verified { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); font-weight: 600; }\n.orgpub-desc { max-width: 720px; color: var(--ink-2); margin: 18px 0 6px; white-space: pre-line; }\n.orgpub .results-head h2 { display: inline-flex; align-items: center; gap: 8px; }\n@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n\n/* ================= Éditeur de profil (photo, bio, réseaux) ================= */\n.profil-edit { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--line-strong); }\n/* profil géré au hub Groupe KA — lecture seule */\n.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }\n.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }\n.hub-profile a.stat-chip:hover { background: var(--blue-soft); }\n.hub-profile .btn-primary { display: inline-flex; align-items: center; gap: 7px; }\n.pe-avatar-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.pe-avatar {\n width: 84px; height: 84px; border-radius: var(--r-pill); object-fit: cover;\n border: 2.5px solid var(--orange); box-shadow: var(--shadow-flat);\n display: flex; align-items: center; justify-content: center;\n}\n.pe-avatar-empty { background: var(--orange-soft); color: var(--orange); font-family: var(--font-display); font-weight: 700; font-size: 34px; }\n.pe-avatar-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }\n.pe-avatar-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n.pe-hint { font-size: 11px; color: var(--ink-3); }\n.pe-socials-title { font-size: 15px; margin-top: 6px; }\n.pe-socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }\n.pe-social { display: flex !important; flex-direction: row !important; align-items: center; gap: 0 !important; }\n.pe-social-ico {\n display: flex; align-items: center; justify-content: center;\n width: 40px; align-self: stretch; flex: none;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-ctl) 0 0 var(--r-ctl);\n}\n.pe-social input { flex: 1; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; border-left: 0; }\n\n/* profil public — à propos + réseaux */\n.pub-about { max-width: 640px; margin-top: 22px; }\n.pub-bio { font-size: 15.5px; color: var(--ink); margin: 0 0 12px; white-space: pre-line; }\n.pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.pub-socials { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }\n.pub-social {\n display: flex; align-items: center; justify-content: center;\n width: 44px; height: 44px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: 14px; color: var(--navy);\n transition: all 0.12s ease;\n}\n.pub-social:hover { background: var(--navy); border-color: var(--navy); color: var(--white); transform: translateY(-2px); box-shadow: var(--shadow-off-soft); }\n\n/* -----------------------------------------------------------------------------\n Lou-Ka Maps (framework Ka Maps par Groupe Ka)\n Jetons du design system appliqués au chrome de la carte + sync liste↔carte.\n----------------------------------------------------------------------------- */\n.mapview .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n}\n.mapview { position: relative; overflow: hidden; }\n.mapview .ka-map { position: absolute; inset: 0; }\n\n/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en orange */\n.lmap3d {\n position: relative; height: 340px; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.lmap3d .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n position: absolute; inset: 0;\n}\n.lmap3d-skel {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.lmap3d-legende {\n position: absolute; left: 10px; bottom: 10px; z-index: 5;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 4px 10px; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n font-size: 10.5px; font-weight: 500; color: var(--ink-2);\n pointer-events: none;\n}\n.lmap3d-legende i {\n width: 10px; height: 10px; border-radius: 3px;\n background: var(--orange); border: 1px solid rgba(20, 24, 20, 0.25);\n}\n@media (max-width: 780px) { .lmap3d { height: 280px; } }\n.ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }\n.ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }\n.ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }\n\n/* carte de liste synchronisée avec la carte (survol / sélection) */\n.map-card { border-radius: var(--r-card); }\n.map-card-sel { outline: 2px solid var(--blue); outline-offset: 2px; }\n\n/* =============================================================================\n HARMONISATION « ÉDITORIAL SHARP » (Groupe KA) — couche finale posée après\n les styles historiques : géométrie 10/6 px, bordures encre 1,5 px, ombres\n décalées dures, accent orange Lou-Ka. La carte Ka Maps (Mapbox) conserve\n son propre thème marine/orange à l'intérieur du canevas.\n============================================================================= */\n\n/* — contrôles : plus de pilules (les chips/badges gardent --r-pill) — */\n.nav a, .login-btn, .cta, .mv-pop-cta, .f-search, .f-more, .pager-btn,\n.onglet, .view-toggle, .fab, .f-ctl, .pass-gestionnaire, .hbar-track,\n.q-bar-track, .pass-progress { border-radius: var(--r-ctl); }\n.seg button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); }\n.seg button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }\n\n/* — header : papier translucide + trait d'encre — */\n.header {\n background: rgba(245, 243, 238, 0.92);\n border-bottom: 1.5px solid var(--ink);\n}\n.nav a { font-family: var(--font-display); }\n.nav a:hover { color: var(--accent-deep); background: var(--accent-soft); }\n.nav a.active { background: var(--ink); color: var(--accent); }\n.brand .ka {\n background: var(--accent); color: var(--on-accent);\n border-radius: 6px; padding: 0 6px 2px; margin-left: 2px; transform: rotate(-2deg);\n}\n.header .gk-badge { margin-left: 4px; flex: none; }\n@media (max-width: 1080px) { .header .gk-badge { display: none; } }\n.mm-badge {\n display: flex; align-items: center; min-height: var(--touch);\n padding: 8px 10px 2px;\n}\n\n/* — ticker : encre + mono — */\n.ticker { font-family: var(--font-mono); border-bottom: 1.5px solid var(--ink); }\n\n/* — états sélectionnés : encre + accent (fini le bleu) — */\n.seg button.on, .onglet.on, .view-toggle button.on, .f-more.on {\n background: var(--ink); border-color: var(--ink); color: var(--accent);\n}\n.pager-btn.on { background: var(--ink); border-color: var(--ink); color: var(--accent); }\n\n/* — CTA / FAB : aplat accent, bordure encre, ombre décalée — */\n.cta, .mv-pop-cta, .fab {\n background: var(--accent); color: var(--on-accent);\n border: 1.5px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);\n font-family: var(--font-display); font-weight: 700;\n}\n.cta:hover, .mv-pop-cta:hover, .fab:hover { background: var(--accent-deep); }\n.cta:active, .fab:active { box-shadow: none; }\n.fab { box-shadow: 4px 4px 0 var(--ink), 0 10px 28px rgba(20, 24, 20, 0.25); }\n.login-btn {\n background: var(--ink); color: var(--accent);\n border: 1.5px solid var(--ink); box-shadow: none;\n font-family: var(--font-display); font-weight: 700;\n}\n.login-btn:hover { background: var(--accent-deep); color: var(--on-accent); }\n\n/* — cartes : levée nette diagonale (signature du groupe) — */\n.card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }\n.role-card:hover:not(:disabled) { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); border-color: var(--ink); }\n.badge { border: 1.5px solid var(--ink); border-radius: var(--r-ctl); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.badge.type { background: var(--accent); color: var(--on-accent); }\n.source-tag { font-family: var(--font-mono); }\n\n/* — barre d'onglets mobile : papier + encre, actif = accent orange — */\n@media (max-width: 760px) {\n .tabbar { background: rgba(245, 243, 238, 0.95); border-top: 1.5px solid var(--ink); }\n .tabbar a { font-family: var(--font-display); }\n .tabbar a.active { color: var(--accent); }\n}\n.mobile-menu { background: var(--paper); border-bottom: 1.5px solid var(--ink); }\n\n/* — carte de membre KA : encre plutôt que marine — */\n.pc { background: linear-gradient(135deg, #1d231d 0%, var(--ink) 55%, #0b0e0b 100%); }\n\n/* — micro-étiquettes en mono (système) — */\n.f-ctl > span, .f-group > label, .field label, .tile-k, .pg-label,\n.pc-label, .pc-id-label, .crumbs, .viz-sub { font-family: var(--font-mono); }\n\n/* — page Contact (design system) — */\n.contact-page { padding: 44px 0 90px; }\n.contact-page h1 { margin: 10px 0 8px; }\n.contact-page .lede { color: var(--ink-2); max-width: 640px; margin: 0 0 26px; }\n.contact-grid { display: grid; gap: 16px; grid-template-columns: 1fr; margin: 26px 0; }\n@media (min-width: 768px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }\n.contact-card { padding: 18px 20px; }\n.contact-card .klabel { display: block; margin-bottom: 6px; }\n.contact-card a.mail {\n font-family: var(--font-mono); font-weight: 700; font-size: 13.5px;\n color: var(--accent-deep); word-break: break-all;\n text-decoration: underline; text-underline-offset: 3px;\n display: inline-flex; align-items: center; min-height: 44px;\n}\n.contact-card p { margin: 6px 0 0; color: var(--ink-2); font-size: 13.5px; }\n.contact-note {\n border-left: 3px solid var(--accent); background: var(--surface);\n border-top: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);\n border-bottom: 1.5px solid var(--ink); border-radius: 0 var(--r-card) var(--r-card) 0;\n padding: 14px 18px; max-width: 720px; color: var(--ink-2); font-size: 14px;\n}\n.contact-note b { color: var(--ink); }\n.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.contact-sites a {\n display: inline-flex; align-items: center; min-height: 34px;\n padding: 4px 12px; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); font-family: var(--font-mono); font-size: 11px;\n font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;\n transition: transform 0.12s, box-shadow 0.12s;\n}\n.contact-sites a:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.contact-hub { margin-top: 26px; }\n\n/* — rangée de navigation locale au-dessus du footer commun — */\n.prefooter { border-top: 1.5px solid var(--ink); background: var(--surface-2); padding: 18px 0; }\n.prefooter .container {\n display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px;\n font-family: var(--font-mono); font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n}\n.prefooter a, .prefooter .flink {\n color: var(--ink-2); text-decoration: none; text-transform: uppercase;\n letter-spacing: 0.08em; font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;\n min-height: var(--touch); display: inline-flex; align-items: center;\n}\n.prefooter a:hover, .prefooter .flink:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }\n.prefooter .baseline { width: 100%; font-family: var(--font-body); font-weight: 400; font-size: 13px; color: var(--ink-2); text-transform: none; letter-spacing: 0; }\n.ka-footer { margin-top: 0; }\n@media (max-width: 760px) { .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } }\n\n/* ============ Juste valeur (fair value) — pastilles & analyse de prix ====== */\n.fv-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border-radius: var(--r-pill); padding: 6px 12px;\n font-size: 12.5px; font-weight: 700; border: 1.5px solid var(--ink);\n white-space: nowrap;\n}\n.fv-badge .fv-ico { font-size: 10px; line-height: 1; }\n.fv-badge .fv-pct { margin-left: 4px; }\n.fv-badge.fv-compact { padding: 2px 8px; font-size: 11px; gap: 4px; margin-left: 8px; vertical-align: 2px; }\n/* états (mêmes jetons que les badges marché existants — daltonien-compatible :\n l'icône ▼ ≈ ▲ et le libellé portent l'information, pas la couleur seule) */\n.fv-badge.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.35); color: var(--blue); }\n.fv-badge.deal-ok { background: var(--surface); color: var(--ink-2); }\n.fv-badge.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.4); color: var(--orange-deep); }\n\n.f-fairvalue .fv-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 12px; flex-wrap: wrap; margin-bottom: 6px;\n}\n.f-fairvalue .fv-value { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--navy); }\n.f-fairvalue .fv-value small { font-size: 12px; font-weight: 500; color: var(--ink-3); }\n.f-fairvalue .fv-range { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.fv-histo { display: block; width: 100%; height: auto; margin: 10px 0 4px; }\n.fv-histo-lbl { font: 700 10px Inter, system-ui, sans-serif; fill: var(--ink); }\n.fv-histo-lbl-fv { fill: var(--accent-deep, #cc5500); }\n.fv-histo-axis { font: 500 9px Inter, system-ui, sans-serif; fill: var(--ink-3); }\n.fv-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12.5px; color: var(--ink-2); margin: 6px 0 4px; }\n.fv-demo { display: flex; flex-wrap: wrap; gap: 8px; }\n.chip.chip-deal.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* =============================================================================\n MapSearch — recherche synchronisée liste ↔ carte (search/MapSearch.tsx)\n Une seule vérité partagée : compteur, tri, survols et sélections miroirs.\n============================================================================= */\n\n.ms { position: relative; }\n.ms-list { position: relative; scroll-behavior: smooth; }\n\n.ms-list-head {\n display: flex; align-items: center; justify-content: space-between;\n gap: 10px; flex-wrap: wrap; padding: 2px 2px 4px;\n}\n.ms-count { font-size: 13.5px; color: var(--ink-2); }\n.ms-count b { color: var(--ink); font-size: 15px; }\n.ms-nopos {\n margin-left: 8px; font-size: 11px; color: var(--ink-3);\n border: 1px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 8px; cursor: help;\n}\n.ms-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }\n.ms-sort select {\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); padding: 6px 8px; font-size: 12.5px;\n font-weight: 600; color: var(--ink);\n}\n\n.ms-pills { margin: 0 0 4px; }\n\n.ms-refit {\n align-self: flex-start; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--ink-2);\n cursor: pointer; box-shadow: var(--shadow-off-soft);\n}\n.ms-refit:hover { background: var(--navy); color: var(--white); border-color: var(--navy); }\n\n/* barre de progression fine — jamais d'écran qui clignote */\n.ms-progress {\n position: sticky; top: 0; z-index: 4; height: 3px; border-radius: 2px;\n background: linear-gradient(90deg, transparent, var(--accent), transparent);\n background-size: 200% 100%; animation: ms-progress 1s linear infinite;\n}\n@keyframes ms-progress { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n.ms-empty {\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-card);\n padding: 26px 18px; text-align: center; color: var(--ink-2);\n}\n.ms-empty .big { margin-bottom: 6px; color: var(--ink-3); }\n.ms-empty h3 { margin: 0 0 6px; font-size: 16px; color: var(--ink); }\n.ms-empty p { margin: 0 0 14px; font-size: 13px; }\n.ms-empty-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n/* miroirs survol / sélection venus de la carte */\n.map-card { transition: transform 0.15s ease, box-shadow 0.15s ease; }\n.map-card-hover {\n outline: 2px solid var(--accent); outline-offset: 2px;\n transform: translateY(-1px);\n}\n.map-card-pulse { animation: ms-pulse 0.9s ease 1; }\n@keyframes ms-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* indicateur discret : l'annonce survolée sur la carte est hors écran */\n.ms-hover-hint {\n position: sticky; z-index: 5; align-self: center;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 5px 12px;\n font-size: 11.5px; font-weight: 600; pointer-events: none;\n box-shadow: var(--shadow-off-soft);\n}\n.ms-hover-hint.haut { top: 8px; order: -1; }\n.ms-hover-hint.bas { bottom: 8px; }\n\n/* ---------------- carrousel mobile synchronisé aux marqueurs ------------- */\n.ms-carousel {\n position: absolute; left: 0; right: 0; bottom: 10px; z-index: 12;\n display: flex; gap: 10px; overflow-x: auto; padding: 4px 12px;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n.ms-carousel::-webkit-scrollbar { display: none; }\n.ms-car-item {\n flex: 0 0 78%; max-width: 340px; scroll-snap-align: center;\n display: grid; grid-template-columns: 108px 1fr; gap: 0;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); overflow: hidden;\n box-shadow: var(--shadow-off-soft); text-decoration: none; color: inherit;\n}\n.ms-car-item.sel { outline: 2.5px solid var(--accent); outline-offset: 1px; }\n.ms-car-item img, .ms-car-item .smart-img, .ms-car-item .card-fallback {\n width: 108px; height: 96px; object-fit: cover; display: block;\n}\n.ms-car-body { padding: 9px 12px; min-width: 0; }\n.ms-car-price { font-size: 14.5px; font-weight: 700; color: var(--ink); display: flex; gap: 6px; align-items: center; }\n.ms-car-title {\n font-size: 12px; color: var(--ink-2); margin-top: 2px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.ms-car-meta { font-size: 11px; color: var(--ink-3); margin-top: 3px; }\n.ms-car-more {\n flex: 0 0 auto; scroll-snap-align: center; align-self: stretch;\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n background: var(--navy); color: var(--white);\n font-size: 12.5px; font-weight: 600; padding: 0 18px; cursor: pointer;\n}\n\n/* mobile : carte plein cadre, liste masquée, carrousel par-dessus la carte */\n@media (max-width: 780px) {\n .ms { height: calc(100dvh - 190px); }\n .ms-list { display: none; }\n .ms .mapview { height: 100%; }\n .ka-search-area { top: 8px; }\n}\n@media (min-width: 781px) {\n .ms-carousel { display: none; }\n}\n\n/* mobile : réorganisation du chrome carte autour du carrousel */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 56px; left: 10px; bottom: auto; }\n .ms .ka-loading { bottom: 132px; }\n .ms .ka-empty { transform: translate(-50%, -70%); }\n}\n/* l'auto-défilement vers la carte laisse l'entête de résultats visible */\n.ms { scroll-margin-top: 108px; }\n\n/* mobile, vue carte : chrome sans chevauchement (case en haut, outils sous\n elle, FAB Filtres au-dessus du carrousel) */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 88px; left: 10px; bottom: auto; }\n .ms .ka-locate { top: 88px; right: 10px; }\n .fab-carte { bottom: 148px; }\n}\n\n/* =============================================================================\n KA Scores — pastilles, jauges circulaires, page méthodologie\n============================================================================= */\n.ka-badge {\n display: inline-flex; align-items: center; gap: 4px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 1px 8px 1px 3px; font-size: 11px; font-weight: 700;\n color: var(--ink); background: var(--surface); line-height: 1.5;\n vertical-align: middle;\n}\n.ka-badge-logo {\n background: var(--ink); color: var(--white); border-radius: 999px;\n font-size: 8.5px; font-weight: 800; padding: 1.5px 5px; letter-spacing: 0.4px;\n}\n.ka-badge.ka-haut { border-color: var(--green, #256d43); }\n.ka-badge.ka-haut .ka-badge-logo { background: var(--green, #256d43); }\n.ka-badge.ka-bon { border-color: var(--accent); }\n.ka-badge.ka-bon .ka-badge-logo { background: var(--accent); }\n.ka-badge.ka-moyen { opacity: 0.9; }\n.ka-badge.ka-bas { opacity: 0.75; }\n\n.ka-circles, .ka-demo-circles {\n display: flex; gap: 18px; flex-wrap: wrap; margin: 14px 0 6px;\n}\n.ka-circle { text-align: center; width: 92px; }\n.ka-circle-fond {\n fill: none; stroke: var(--line); stroke-width: 6;\n}\n.ka-circle-arc {\n fill: none; stroke: var(--ink-3); stroke-width: 6; stroke-linecap: round;\n transition: stroke-dasharray 0.6s ease;\n}\n.ka-circle.ka-haut .ka-circle-arc { stroke: var(--green, #256d43); }\n.ka-circle.ka-bon .ka-circle-arc { stroke: var(--accent); }\n.ka-circle.ka-moyen .ka-circle-arc { stroke: var(--orange, #d97a2b); }\n.ka-circle.ka-bas .ka-circle-arc { stroke: #b3543f; }\n.ka-circle-val { font: 700 17px var(--font-body); fill: var(--ink); }\n.ka-circle-nom { font-size: 12px; font-weight: 700; color: var(--ink); margin-top: 4px; }\n.ka-circle-label { font-size: 10.5px; color: var(--ink-3); }\n\n.f-kascores h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ka-perso {\n font-size: 12px; font-weight: 600; color: var(--ink-2);\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 10px;\n}\n.ka-detail summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n padding: 6px 0;\n}\n.ka-detail-grille {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px; padding: 8px 0 4px;\n}\n.ka-detail-grille h4 { margin: 0 0 4px; font-size: 12px; color: var(--ink); }\n.ka-detail-grille ul { margin: 0 0 10px; padding-left: 16px; font-size: 12.5px; color: var(--ink-2); }\n.ka-detail-grille li { margin: 2px 0; }\n\n.ka-poids { display: grid; gap: 10px; margin: 14px 0; max-width: 460px; }\n.ka-poids-row {\n display: grid; grid-template-columns: 90px 1fr 48px; align-items: center;\n gap: 12px; font-size: 13px; font-weight: 600; color: var(--ink);\n}\n.ka-poids-row input[type=\"range\"] { accent-color: var(--accent); }\n.ka-poids-row b { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ms-car-price .ka-badge { margin-left: 2px; }\n@media (max-width: 780px) {\n .ms .ka-3d { top: 148px; right: 10px; }\n}\n\n\n/* ---- Registre des loyers (fiche) — bloc citoyen registre-des-loyers.ca ---- */\n.f-rdl .rdl-resume { margin: 2px 0 6px; }\n.f-rdl .rdl-ecart { margin: 0 0 8px; }\n.rdl-rooms { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 10px; }\n.rdl-room { font-size: 13px; color: var(--ink-2); white-space: nowrap; }\n.rdl-room i { font-style: normal; color: var(--ink-3); }\n.rdl-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.rdl-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line, #e6e4df);\n vertical-align: top; }\n.rdl-table .rdl-addr { max-width: 46%; }\n.rdl-table .rdl-prix { font-weight: 600; white-space: nowrap; }\n.rdl-table .rdl-date { white-space: nowrap; color: var(--ink-2); }\n.rdl-table .rdl-dist { color: var(--ink-3); white-space: nowrap; text-align: right; }\n.rdl-room-on { background: var(--sand, #f4efe7); border-radius: 6px; padding: 2px 8px; }\n\n\n/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */\n.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }\n.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n font-size: 12.5px; font-weight: 700; }\n.zi-eleve { background: #fde8e8; color: #a12622; }\n.zi-modere { background: #fdf3e0; color: #8a5a00; }\n.zi-present { background: #fdf3e0; color: #8a5a00; }\n.zi-ok { background: #e7f4ea; color: #1e6b34; }\n.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n\n\n/* ---- Historique TAL (fiche) — décisions SOQUIJ à l'adresse ---- */\n.f-tal .tal-pending, .f-tal .tal-ok, .f-tal .tal-resume { margin: 2px 0 8px; }\n.tal-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.tal-liste li { padding: 8px 0; border-top: 1px solid var(--line, #e6e4df); }\n.tal-ligne { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }\n.tal-date { font-weight: 600; color: var(--ink-2); }\n.tal-part { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: var(--sand, #f0ede8); color: var(--ink-2); }\n.tal-part-locateur { background: #fdf3e0; color: #8a5a00; }\n.tal-verdict { font-size: 12px; color: var(--ink-3); }\n.tal-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 5px 0 3px; }\n.tal-tag { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: #fde8e8; color: #a12622; }\n.tal-ref { font-size: 12.5px; color: var(--ink-3); }\n\n\n/* ---- Registre des loyers v2 : tuiles, bande de position, barres ---- */\n.rdl-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 12px; }\n.rdl-kpi { flex: 1 1 90px; min-width: 90px; background: var(--surface-2, #f7f5f1);\n border-radius: 10px; padding: 10px 12px; }\n.rdl-kpi-v { display: block; font-size: 20px; font-weight: 700;\n letter-spacing: -0.02em; color: var(--navy); }\n.rdl-kpi-l { display: block; font-size: 11px; line-height: 1.35;\n color: var(--ink-3); margin-top: 2px; }\n.rdl-strip { width: 100%; height: auto; display: block; margin: 2px 0 4px; }\n.rdl-strip-track { fill: var(--surface-2, #f0ede8); }\n.rdl-strip-box { fill: var(--accent-soft, #fff1e6);\n stroke: var(--accent, #ff6a00); stroke-width: 1; }\n.rdl-strip-med { stroke: var(--ink-2, #4c4a45); stroke-width: 2; }\n.rdl-strip-me { stroke: var(--accent-deep, #cc5500); stroke-width: 2; }\n.rdl-strip-me-dot { fill: var(--accent-deep, #cc5500);\n stroke: var(--surface, #fff); stroke-width: 2; }\n.rdl-strip-lbl, .rdl-strip-axis { font-size: 10.5px; fill: var(--ink-3); }\n.rdl-strip-me-lbl { font-size: 11px; font-weight: 700;\n fill: var(--accent-deep, #cc5500); }\n.rdl-bars { width: 100%; height: auto; display: block; margin: 6px 0 2px; }\n.rdl-bar { fill: var(--accent, #ff6a00); opacity: 0.55; }\n.rdl-bar-on { fill: var(--accent-deep, #cc5500); opacity: 1; }\n.rdl-bar-val { font-size: 10.5px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat { font-size: 11px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat-on { font-weight: 700; fill: var(--ink, #1c1b18); }\n.rdl-bar-n { font-size: 9.5px; fill: var(--ink-3, #8a877f); }\n.rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }\n.rdl-ecart { margin: 4px 0 8px; }\n.rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }\n\n\n/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */\n.q-crime { width: 100%; }\n.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }\n.q-crime-cats li { display: flex; align-items: center; gap: 8px;\n padding: 3px 0; font-size: 12.5px; }\n.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.q-crime-barre i { display: block; height: 100%; border-radius: 5px;\n background: var(--accent, #ff6a00); opacity: 0.6; }\n.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; }\n.q-crime-n small { font-weight: 400; color: var(--ink-3); }\n.q-crime-src { margin-top: 8px; }\n\n\n/* ---- Qualité de l'air (RSQAQ) + Essence à proximité ---- */\n.air-liste { list-style: none; margin: 10px 0 0; padding: 0; }\n.air-liste li { display: flex; align-items: center; gap: 8px;\n padding: 4px 0; font-size: 12.5px; }\n.air-nom { flex: 0 0 44%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.air-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.air-barre i { display: block; height: 100%; border-radius: 5px;\n background: #4d9e64; }\n.air-barre i.air-sur { background: #c96a1f; }\n.air-val { flex: 0 0 34%; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.air-val small { font-weight: 400; color: var(--ink-3); }\n.air-val .air-ref { display: block; font-size: 10px; }\n.gaz-head th { text-align: left; font-size: 11px; color: var(--ink-3);\n font-weight: 600; padding: 2px 8px 2px 0; }\n.gaz-best { display: inline-block; margin-left: 6px; padding: 1px 7px;\n border-radius: 999px; background: #e7f4ea; color: #1e6b34;\n font-size: 10.5px; font-weight: 700; }\n.gaz-adr { display: block; font-size: 11px; color: var(--ink-3); }\n\n\n/* ---- Commerces et transport (fiche) ---- */\n.cm-grille { list-style: none; margin: 6px 0 0; padding: 0;\n display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }\n@media (max-width: 560px) { .cm-grille { grid-template-columns: 1fr; } }\n.cm-item { display: flex; align-items: center; gap: 9px; padding: 5px 0;\n border-bottom: 1px solid var(--line, #eeece7); min-width: 0; }\n.cm-ico { flex: 0 0 28px; }\n.cm-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.cm-nom { font-size: 13px; font-weight: 600; color: var(--ink, #222);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-poi { font-size: 11px; color: var(--ink-3, #8a877f);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-dist { flex: 0 0 auto; font-size: 13px; font-weight: 700;\n color: var(--ink-2, #4c4a45); white-space: nowrap; }\n\n\n/* ---- Coût d'électricité (Hydro-Québec) ---- */\n.hydro-intro { margin: 2px 0 12px; color: var(--ink-2, #4c4a45); }\n.hydro-btn { appearance: none; border: 0; border-radius: 10px; cursor: pointer;\n background: var(--accent, #2b7a3b); color: #fff; font-weight: 700;\n font-size: 14px; padding: 11px 18px; }\n.hydro-btn:disabled { opacity: 0.6; cursor: progress; }\n.hydro-montant { font-size: 26px; font-weight: 800; letter-spacing: -0.02em;\n color: var(--navy, #1c1b18); display: flex; align-items: baseline;\n gap: 10px; flex-wrap: wrap; }\n.hydro-montant small { font-size: 14px; font-weight: 500; color: var(--ink-3); }\n.hydro-an { font-size: 14px; font-weight: 600; color: var(--ink-2, #4c4a45); }\n.hydro-vide { color: var(--ink-3, #8a877f); }\n\n\n/* ============================================================================\n PASSE DE DESIGN v3 — « éditorial sharp » appliqué à toute la fiche\n Blocs = cartes encre (bordure 1,5 px + ombre décalée douce), titres à\n marqueur accent + filet, tuiles KPI blanches à ombre dure, tableaux zébrés,\n barres en dégradé, pastilles unifiées (mono, point d'état, liseré).\n Aucune propriété `order` (règle Groupe Ka : ordre DOM = ordre visuel).\n ========================================================================== */\n\n/* --- Blocs de la fiche : cartes posées sur le papier --- */\n.f-bloc:not(.f-galerie):not(.f-hero) {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-card);\n padding: 18px 20px 16px;\n box-shadow: var(--shadow-off-soft);\n}\n@media (max-width: 640px) {\n .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n}\n\n/* --- Titres de section : carré accent + filet éditorial --- */\n.f-bloc h2, .quartier h2 {\n display: flex; align-items: center; gap: 10px;\n font-size: 19px; letter-spacing: -0.015em; margin-bottom: 14px;\n}\n.f-bloc h2::before, .quartier h2::before {\n content: \"\"; flex: 0 0 9px; width: 9px; height: 9px;\n background: var(--accent); border: 1.5px solid var(--ink);\n box-shadow: 2px 2px 0 var(--ink);\n}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1px solid currentColor; border-radius: var(--r-pill, 999px);\n white-space: nowrap;\n}\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--line);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n","structuredPatch":[{"oldStart":2124,"oldLines":9,"newStart":2124,"newLines":10,"lines":[" display: inline-block;"," font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;"," text-transform: uppercase; letter-spacing: 0.06em;","- padding: 2px 7px; border: 1px solid currentColor; border-radius: var(--r-pill, 999px);","- white-space: nowrap;","+ padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;","+ white-space: nowrap; vertical-align: 1px;"," }","+.cr-poste .st-pill { margin-left: 8px; }"," .st-included { color: #2e7d4a; background: #eef7f0; }"," .st-observed { color: var(--ink); background: var(--surface); }"," .st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMd4hZN8aer28XBLFco","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01AFsSn81gWMTEbG6LWVAKa8","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","old_string":".cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }","new_string":".cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }\n.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2672,"cache_read_input_tokens":104805,"cache_creation":{"ephemeral_5m_input_tokens":2672,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"836020c5-d941-4621-bbad-a1448b6038eb"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01AFsSn81gWMTEbG6LWVAKa8","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e7f43de2-3628-4f48-9133-448fafcaa20a","timestamp":"2026-08-24T22:24:11.786Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","oldString":".cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }","newString":".cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }\n.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }","originalFile":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }\n\n/* Garde-fou : jamais de défilement horizontal de la page, même transitoire\n pendant le chargement (bandeau défilant, polices) — clip ne casse pas\n position: sticky, contrairement à hidden. */\nhtml, body { overflow-x: clip; }\n\n/* ================= Header ================= */\n.header {\n position: sticky; top: 0; z-index: var(--z-header, 500);\n background: var(--paper);\n border-bottom: 1px solid var(--line);\n}\n.header-inner { display: flex; align-items: center; gap: 20px; height: 64px; }\n.brand {\n font-family: var(--font-display); font-weight: 700; font-size: 25px;\n letter-spacing: -0.03em; display: flex; align-items: center; line-height: 1;\n color: var(--navy);\n}\n.brand .logo-ico { margin-right: 12px; flex: none; } /* ≈ 40 % de la largeur de l'icône */\n.brand .ka { color: var(--orange); }\n.brand-tag {\n font-size: 11px; font-weight: 500; color: var(--ink-3);\n letter-spacing: 0.02em; margin-left: 12px;\n}\n@media (max-width: 900px) { .brand-tag { display: none; } }\n.nav { margin-left: auto; display: flex; gap: 4px; }\n.nav a {\n padding: 9px 16px; border-radius: var(--r-pill); font-weight: 600; font-size: 14px;\n color: var(--navy); transition: all 0.15s ease; min-height: 40px;\n display: inline-flex; align-items: center;\n}\n.nav a:hover { color: var(--orange); background: var(--orange-soft); }\n.nav a.active { background: var(--navy); color: var(--white); }\n\n/* ================= Ticker ================= */\n.ticker {\n background: var(--navy); color: rgba(255, 255, 255, 0.85); overflow: hidden;\n font-size: 11.5px; font-weight: 500; letter-spacing: 0.08em;\n text-transform: uppercase; padding: 7px 0; white-space: nowrap;\n}\n.ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }\n.ticker span { padding: 0 26px; position: relative; }\n.ticker span::after { content: \"◆\"; position: absolute; right: -6px; color: var(--orange); opacity: 0.8; font-size: 8px; top: 3px; }\n@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) {\n .ticker-track { animation: none; }\n * { transition-duration: 0.01ms !important; }\n}\n\n/* ================= Hero ================= */\n.hero { padding: 58px 0 22px; }\n@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n.hero h1 {\n font-size: clamp(36px, 5.4vw, 56px); font-weight: 700; line-height: 1.06;\n letter-spacing: -0.03em; max-width: 900px; margin-top: 14px; color: var(--navy);\n}\n.hero h1 .outline { color: var(--navy); }\n.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }\n.stat-chip {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 8px 16px; font-size: 12.5px; font-weight: 500;\n color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n box-shadow: var(--shadow-flat);\n}\n.stat-chip b { color: var(--navy); font-weight: 700; }\n.stat-chip .pulse {\n width: 8px; height: 8px; border-radius: 50%; background: var(--orange);\n box-shadow: 0 0 0 4px var(--orange-soft); animation: pulse 2.4s ease infinite;\n}\n@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 106, 0, 0.18); } }\n\n/* ================= Rangée d'avantages ================= */\n.benefits {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px; margin-top: 30px;\n}\n.benefit { display: flex; align-items: center; gap: 12px; }\n.benefit-ico {\n width: 42px; height: 42px; border-radius: var(--r-pill); flex: none;\n display: flex; align-items: center; justify-content: center;\n background: var(--orange-soft); color: var(--orange);\n}\n.benefit-ico.blue { background: var(--blue-soft); color: var(--blue); }\n.benefit-label {\n font-size: 11.5px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--navy);\n}\n@media (max-width: 640px) {\n .benefits { grid-template-columns: 1fr 1fr; gap: 12px; }\n}\n\n/* ================= Filter bar ================= */\n.filterbar {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 14px; margin: 30px 0 6px;\n display: flex; flex-direction: column; gap: 0; min-width: 0;\n}\n\n/* — rangée principale — */\n.f-primary { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; min-width: 0; }\n.f-search {\n flex: 1 1 220px; min-width: 0; max-width: 440px; align-self: center;\n display: flex; align-items: center; gap: 8px;\n border: 1px solid transparent; border-radius: var(--r-pill); background: var(--grey);\n padding: 0 15px; min-height: 42px; color: var(--ink-2);\n transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;\n}\n.f-search:focus-within {\n background: var(--white); border-color: var(--blue);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n.f-search input {\n border: none; background: none; outline: none; flex: 1; min-width: 0;\n font-size: 14px; color: var(--ink); font-family: inherit;\n}\n.f-clear {\n border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n width: 20px; height: 20px; font-size: 10px; cursor: pointer; flex: none;\n display: grid; place-items: center;\n}\n.f-ctl {\n flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center;\n gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n padding: 7px 14px 6px; min-height: 52px; cursor: pointer;\n transition: box-shadow 0.15s ease, border-color 0.15s ease;\n}\n.f-ctl:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.f-ctl > span {\n font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-ctl select {\n border: none; background: transparent; outline: none; font-family: var(--font-display);\n font-weight: 700; font-size: 14.5px; color: var(--navy); cursor: pointer;\n appearance: none; -webkit-appearance: none; padding-right: 16px; min-width: 0; max-width: 170px;\n text-overflow: ellipsis;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right center;\n}\n.range-pair { display: flex; align-items: center; gap: 4px; }\n.range-pair select { max-width: 86px; }\n.range-sep { color: var(--ink-3); font-size: 11px; }\n.f-more {\n flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n transition: all 0.13s ease;\n}\n.f-more:hover { background: var(--blue-soft); border-color: var(--blue); color: var(--blue); }\n.f-more.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* — panneau avancé — */\n.f-adv {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 22px;\n border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n animation: adv-in 0.18s ease;\n}\n@keyframes adv-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }\n.f-group { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.f-group > label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-group-end { justify-content: flex-end; }\n.f-group .btn:disabled { opacity: 0.4; cursor: default; }\n.f-native {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 10px 30px 10px 14px; font-size: 14px; color: var(--ink); outline: none;\n font-family: inherit; min-height: 42px; width: 100%; min-width: 0;\n appearance: none; -webkit-appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center;\n}\n.f-native:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); background: var(--white); }\n\n/* segments (pilules soudées) */\n.seg { display: inline-flex; flex-wrap: wrap; row-gap: 6px; }\n.seg button {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink-2);\n padding: 8px 14px; font-family: var(--font-display); font-weight: 600; font-size: 13px;\n cursor: pointer; margin-left: -1px; white-space: nowrap; min-height: 38px;\n transition: all 0.12s ease;\n}\n.seg button:first-child { border-radius: var(--r-pill) 0 0 var(--r-pill); margin-left: 0; padding-left: 16px; }\n.seg button:last-child { border-radius: 0 var(--r-pill) var(--r-pill) 0; padding-right: 16px; }\n.seg button:hover { background: var(--blue-soft); color: var(--blue); }\n.seg button.on { background: var(--blue); border-color: var(--blue); color: var(--white); position: relative; z-index: 1; }\n\n/* pastilles de filtres actifs */\n.pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }\n.pill {\n display: inline-flex; align-items: center; gap: 7px;\n border: 1px solid rgba(28, 92, 65, 0.3); background: var(--blue-soft); color: var(--blue);\n border-radius: var(--r-pill); padding: 6px 14px; font-size: 12.5px; font-weight: 600;\n cursor: pointer; transition: all 0.13s ease;\n}\n.pill:hover { background: var(--blue); color: var(--white); }\n.pill-x { font-size: 10px; opacity: 0.65; }\n.pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }\n.pill-clear:hover { background: var(--danger); color: #fff; }\n.chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 4px 4px; flex: none; }\n.link-btn {\n background: none; border: none; padding: 0; color: var(--blue); font: inherit;\n text-decoration: underline; cursor: pointer;\n}\n.field { display: flex; flex-direction: column; gap: 5px; }\n.field label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); padding-left: 2px;\n}\n.field input, .field select {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 11px 14px; font-size: 15px; color: var(--ink); outline: none; font-family: inherit;\n transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; min-height: 44px;\n appearance: none; -webkit-appearance: none;\n}\n.field select {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px;\n}\n.field input:focus, .field select:focus {\n border-color: var(--blue); background: var(--white);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n/* .btn / .btn-primary / .btn-ghost : fournis par tokens.css (ka-ui). */\n\n/* ================= Chips ================= */\n.chips { display: flex; gap: 8px; margin: 16px 0 4px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }\n.chips::-webkit-scrollbar { display: none; }\n.chip {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink);\n border-radius: var(--r-pill); padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer;\n font-family: var(--font-display); white-space: nowrap; min-height: 40px;\n transition: transform 0.13s, box-shadow 0.13s, background 0.13s, color 0.13s;\n}\n.chip:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.chip.on { background: var(--ink); border-color: var(--ink); color: var(--accent); box-shadow: 3px 3px 0 var(--accent); }\n\n/* ================= Results ================= */\n.results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }\n.results-head h2 { font-size: 22px; letter-spacing: -0.02em; color: var(--navy); }\n.results-head span { color: var(--ink-3); font-size: 12.5px; font-weight: 500; }\n.grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));\n gap: 22px; padding-bottom: 70px;\n}\n@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n\n.card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-flat);\n transition: transform 0.16s ease, box-shadow 0.16s ease;\n}\n.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-off); }\n.card:focus-visible { outline: 3px solid rgba(28, 92, 65, 0.4); outline-offset: 2px; }\n.card-img { position: relative; aspect-ratio: 16/10.5; background: var(--grey); overflow: hidden; }\n.card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }\n.card:hover .card-img img { transform: scale(1.05); }\n.card-img .noimg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--ink-3); font-size: 34px; }\n.badge {\n position: absolute; top: 12px; left: 12px; border-radius: var(--r-pill); padding: 4px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: 0.04em;\n background: rgba(255, 255, 255, 0.94); color: var(--navy);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.badge.type { background: var(--orange); color: var(--white); }\n.badge.right { left: auto; right: 12px; background: rgba(255, 255, 255, 0.92); }\n.card-body { padding: 16px 18px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\n.card-price { font-family: var(--font-display); font-weight: 700; font-size: 21px; letter-spacing: -0.02em; color: var(--navy); }\n.card-price small { font-weight: 500; color: var(--ink-3); font-size: 11.5px; }\n.card-title { font-weight: 600; font-size: 14.5px; color: var(--ink); }\n.card-meta { color: var(--ink-3); font-size: 12.5px; display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }\n.card-meta .sep { width: 4px; height: 4px; background: var(--orange); border-radius: 50%; }\n.card-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.source-tag {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--blue); background: var(--blue-soft);\n border-radius: var(--r-pill); padding: 3px 10px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;\n}\n.avail { font-size: 11.5px; color: var(--ink-2); font-weight: 500; text-align: right; }\n\n/* ================= Skeletons ================= */\n@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }\n.skel { border-radius: var(--r-card); border: 1.5px solid var(--ink); overflow: hidden; background: var(--surface); }\n.skel .sk-img, .skel .sk-line {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.skel .sk-img { aspect-ratio: 16/10.5; }\n.skel .sk-line { height: 14px; border-radius: 4px; margin: 12px 16px; }\n.skel .sk-line.short { width: 45%; }\n\n/* ================= Empty / error ================= */\n.notice { text-align: center; padding: 72px 24px; color: var(--ink-2); }\n.notice .big { font-size: 44px; margin-bottom: 10px; }\n.notice h2 { color: var(--navy); }\n\n/* ================= Detail page ================= */\n.detail { padding: 30px 0 90px; }\n.crumbs {\n font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;\n color: var(--ink-3); margin-bottom: 20px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;\n}\n.crumbs a { border-bottom: 1.5px solid transparent; }\n.crumbs a:hover { color: var(--blue); border-color: var(--blue); }\n.detail-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n/* les enfants de grille ont min-width:auto par défaut : un contenu large\n (grille du quartier) déborderait de l'écran mobile sans ceci */\n.detail-grid > * { min-width: 0; }\n\n.gallery { display: flex; flex-direction: column; gap: 10px; }\n.gallery-main {\n border-radius: var(--r-card); overflow: hidden; border: 1.5px solid var(--ink);\n aspect-ratio: 16/10; background: var(--grey); cursor: zoom-in; box-shadow: var(--shadow-off-soft);\n}\n.gallery-main img { width: 100%; height: 100%; object-fit: cover; }\n.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; }\n.thumbs button {\n border: 2px solid var(--line); border-radius: 12px; overflow: hidden; padding: 0; cursor: pointer;\n aspect-ratio: 4/3; background: var(--grey); transition: border-color 0.12s ease, transform 0.12s ease;\n}\n.thumbs button:hover { transform: translateY(-2px); }\n.thumbs button.on { border-color: var(--orange); }\n.thumbs img { width: 100%; height: 100%; object-fit: cover; }\n\n.panel {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px; position: sticky; top: 120px;\n}\n@media (max-width: 900px) { .panel { position: static; } }\n.panel .price { font-family: var(--font-display); font-size: 34px; font-weight: 700; letter-spacing: -0.03em; color: var(--navy); }\n.panel .price small { font-size: 12.5px; color: var(--ink-3); font-weight: 500; }\n.panel h1 { font-size: 22px; margin: 8px 0 2px; }\n.panel .loc { color: var(--ink-2); font-size: 14px; }\n.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n/* .klabel : fourni par tokens.css (ka-ui). */\n.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n.amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }\n.amenity {\n background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 600;\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-pill); padding: 5px 13px;\n}\n.cta {\n display: block; text-align: center; background: var(--orange); color: var(--white);\n font-weight: 600; font-family: var(--font-display); border-radius: var(--r-pill);\n padding: 15px; min-height: 48px;\n box-shadow: 0 8px 22px rgba(255, 106, 0, 0.34); transition: all 0.14s ease;\n}\n.cta:hover { background: var(--orange-deep); }\n.cta:active { transform: translateY(1px); }\n.panel .fine { font-size: 11.5px; color: var(--ink-3); margin-top: 14px; text-align: center; }\n\n/* ================= Lightbox ================= */\n.lightbox {\n position: fixed; inset: 0; background: rgba(20, 24, 20, 0.94); z-index: var(--z-modal, 900);\n display: flex; align-items: center; justify-content: center; cursor: zoom-out;\n padding: max(16px, env(safe-area-inset-top)) 16px;\n}\n.lightbox img { max-width: 94vw; max-height: 90vh; border-radius: 12px; }\n\n/* Lightbox v2 : balayage entre photos (scroll-snap) + pincement pour zoomer */\n.lightbox-v2 { padding: 0; cursor: default; display: block; }\n.lightbox-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%;\n -webkit-overflow-scrolling: touch; scrollbar-width: none;\n touch-action: pan-x pinch-zoom;\n}\n.lightbox-track::-webkit-scrollbar { display: none; }\n.lightbox-cell {\n flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center;\n display: flex; align-items: center; justify-content: center;\n padding: max(52px, env(safe-area-inset-top)) 10px max(24px, env(safe-area-inset-bottom));\n overflow: hidden;\n}\n.lightbox-cell img {\n max-width: 100%; max-height: 100%; border-radius: 10px;\n user-select: none; -webkit-user-drag: none; will-change: transform;\n}\n.lightbox-close {\n position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.4);\n background: rgba(20, 24, 20, 0.6); color: #fff; font-size: 18px; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.lightbox-count {\n position: fixed; left: 50%; transform: translateX(-50%); right: auto;\n top: max(22px, env(safe-area-inset-top)); bottom: auto; z-index: 3;\n}\n.lightbox-v2 .carousel-nav { position: fixed; z-index: 3; }\n\n/* visuel de secours (SmartImg) directement sous .carousel (galerie vide) */\n.carousel > img { display: block; width: 100%; aspect-ratio: 16/11; object-fit: cover; }\n\n/* ================= Sources page ================= */\n.sources { padding: 44px 0 90px; }\n.sources h1 { font-size: clamp(28px, 4vw, 40px); margin: 10px 0 6px; color: var(--navy); }\n.sources .sub { color: var(--ink-2); margin-bottom: 30px; max-width: 700px; }\n.src-wrap { overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); background: var(--surface); }\n.src-table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 720px; }\n.src-table th {\n text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: 0.1em; color: var(--ink-3); padding: 14px 18px;\n border-bottom: 1px solid var(--line-strong); background: var(--grey);\n}\n.src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }\n.src-table tr:last-child td { border-bottom: none; }\n.src-table tr:hover td { background: var(--navy-soft); }\n.src-table a { color: var(--blue); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.pill { display: inline-block; border-radius: var(--r-pill); padding: 3px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }\n.pill.ok { background: var(--blue-soft); color: var(--blue); }\n.pill.todo { background: var(--orange-soft); color: var(--orange-deep); }\n.count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--navy); }\n\n/* ================= Page Statistiques ================= */\n.stats-page { padding: 44px 0 90px; }\n.stats-title { font-size: clamp(30px, 4.6vw, 46px); margin: 10px 0 6px; color: var(--navy); }\n.stats-page .sub { color: var(--ink-2); max-width: 640px; margin-bottom: 30px; }\n\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 30px; }\n.tile {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n padding: 18px 20px; box-shadow: var(--shadow-flat);\n}\n.tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--navy); }\n.tile-k { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }\n.hero-tile { background: var(--navy); color: var(--white); border-color: var(--navy); }\n.hero-tile .tile-v { color: var(--white); }\n.hero-tile .tile-k { color: rgba(255, 255, 255, 0.6); }\n\n.viz-card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px 28px 20px; margin-bottom: 22px;\n}\n.viz-card h2 { font-size: 19px; letter-spacing: -0.01em; color: var(--navy); }\n.viz-sub { font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 4px 0 20px; }\n.viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n\n/* Histogramme (mono-série, bleu vif) */\n.histo { display: flex; align-items: stretch; gap: 2px; height: 240px; }\n.histo-col { flex: 1; display: flex; flex-direction: column; min-width: 0; cursor: default; }\n.histo-bar-zone { flex: 1; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line-strong); }\n.histo-bar {\n width: 100%; background: var(--blue); border-radius: 6px 6px 0 0;\n min-height: 2px; transition: background 0.12s ease;\n}\n.histo-col:hover .histo-bar { background: var(--orange); }\n.histo-x { font-size: 9.5px; color: var(--ink-3); text-align: left; height: 18px; padding-top: 5px; overflow: visible; white-space: nowrap; }\n\n/* Barres horizontales (mono-série) */\n.hbars { display: flex; flex-direction: column; gap: 7px; }\n.hbar-row {\n display: grid; grid-template-columns: minmax(96px, 170px) 1fr auto;\n gap: 12px; align-items: center; min-height: 26px; cursor: default;\n}\n.hbar-label { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.hbar-label a { border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.hbar-label a:hover { color: var(--blue); }\n.hbar-track { background: var(--grey); border-radius: var(--r-pill); height: 18px; overflow: hidden; }\n.hbar-fill {\n display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill);\n min-width: 2px; transition: background 0.12s ease;\n}\n.hbar-row:hover .hbar-fill { background: var(--orange); }\n.hbar-value { font-size: 11.5px; font-weight: 700; white-space: nowrap; color: var(--navy); }\n.hbar-value em { font-style: normal; font-weight: 500; color: var(--ink-3); }\n@media (max-width: 640px) {\n .hbar-row { grid-template-columns: minmax(80px, 110px) 1fr auto; gap: 8px; }\n .hbar-value em { display: none; }\n .viz-card { padding: 20px 16px 14px; }\n .histo { height: 170px; }\n}\n\n/* Infobulle */\n.viz-tip {\n position: fixed; z-index: var(--z-toast, 950); pointer-events: none; max-width: 180px;\n background: var(--navy); color: var(--white); border-radius: 12px;\n padding: 9px 12px; font-size: 12px; line-height: 1.5;\n box-shadow: 0 8px 24px rgba(20, 24, 20, 0.35);\n}\n.viz-tip-title { font-family: var(--font-display); font-weight: 700; color: var(--orange); margin-bottom: 2px; }\n\n/* Vue tableau repliable (accessibilité / relief) */\n.viz-table { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 10px; }\n.viz-table summary {\n cursor: pointer; font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em; color: var(--blue);\n}\n.viz-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }\n.viz-table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line-strong); }\n.viz-table td { padding: 6px 10px; border-bottom: 1px solid var(--line); }\n\n.stats-foot { font-size: 11.5px; color: var(--ink-3); letter-spacing: 0.02em; margin-top: 6px; }\n\n/* ================= Mobile : feuille de filtres + FAB ================= */\n.sheet-head { display: none; }\n.sheet-apply { display: none; }\n.sheet-backdrop { display: none; }\n.fab { display: none; }\n\n@media (max-width: 640px) {\n /* En-tête compact */\n .header-inner { height: 56px; }\n .brand { font-size: 21px; }\n .nav a { padding: 8px 13px; font-size: 13.5px; }\n .ticker { font-size: 10.5px; padding: 6px 0; }\n\n /* Héro resserré + stats en rangée défilante */\n .hero h1 { font-size: clamp(30px, 8.6vw, 42px); }\n .hero p.lede { font-size: 15px; }\n .stat-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 6px; margin-right: -16px; padding-right: 16px; }\n .stat-row::-webkit-scrollbar { display: none; }\n .stat-chip { flex: 0 0 auto; white-space: nowrap; }\n\n /* La barre de filtres devient une feuille coulissante (bottom sheet) */\n .filterbar { display: none; }\n .filterbar.open .f-primary { flex-direction: column; }\n .filterbar.open .f-search { align-self: stretch; max-width: none; min-height: 46px; }\n .filterbar.open .f-search input { font-size: 16px; } /* anti-zoom iOS */\n .filterbar.open .f-ctl select { max-width: none; width: 100%; }\n .filterbar.open .f-more { display: none; }\n .filterbar.open .f-adv { margin-top: 4px; }\n .filterbar.open {\n display: flex; flex-direction: column; gap: 12px;\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);\n margin: 0; border-radius: 24px 24px 0 0; border: none;\n max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;\n padding: 16px 18px calc(18px + env(safe-area-inset-bottom));\n box-shadow: 0 -16px 48px rgba(20, 24, 20, 0.28);\n animation: sheet-up 0.22s ease;\n }\n @keyframes sheet-up { from { transform: translateY(30%); opacity: 0.4; } to { transform: none; opacity: 1; } }\n .filterbar.open .sheet-head {\n display: flex; justify-content: space-between; align-items: center;\n font-family: var(--font-display); font-weight: 700; font-size: 17px;\n letter-spacing: -0.01em; color: var(--navy);\n position: sticky; top: -16px; background: var(--surface); padding: 6px 0 8px;\n border-bottom: 1px solid var(--line); margin-bottom: 2px; z-index: 1;\n }\n .sheet-close {\n border: 1.5px solid var(--ink); background: var(--surface); border-radius: 50%;\n width: 36px; height: 36px; font-size: 15px; cursor: pointer; line-height: 1; color: var(--navy);\n }\n .filterbar.open .sheet-apply { display: block; width: 100%; }\n .sheet-backdrop {\n display: block; position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.45); backdrop-filter: blur(2px);\n }\n\n /* Bouton flottant */\n .fab {\n display: flex; align-items: center; gap: 6px;\n position: fixed; left: 50%; transform: translateX(-50%);\n bottom: calc(74px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: 80;\n background: var(--orange); color: var(--white); border: none;\n border-radius: var(--r-pill); padding: 13px 24px; font-family: var(--font-display);\n font-weight: 600; font-size: 15px; cursor: pointer;\n box-shadow: 0 10px 28px rgba(255, 106, 0, 0.4);\n }\n .fab:active { transform: translateX(-50%) scale(0.97); }\n\n /* Fiche logement : vignettes en bande défilante + panneau non collant */\n .thumbs { display: flex; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }\n .thumbs::-webkit-scrollbar { display: none; }\n .thumbs button { flex: 0 0 96px; }\n .detail { padding-top: 20px; }\n .panel { padding: 20px; }\n .panel .price { font-size: 28px; }\n .results-head { margin-top: 20px; }\n .chips { margin-right: -16px; padding-right: 16px; }\n .notice { padding: 48px 16px; }\n}\n\n/* ================= Barre de navigation inférieure (mobile) ================= */\n.tabbar { display: none; }\n@media (max-width: 760px) {\n .tabbar {\n display: grid; grid-template-columns: repeat(4, 1fr);\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border-top: 1px solid var(--line);\n padding: 6px 4px calc(6px + env(safe-area-inset-bottom));\n }\n .tabbar a {\n display: flex; flex-direction: column; align-items: center; gap: 3px;\n padding: 5px 2px; min-height: 48px; justify-content: center;\n font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n border-radius: var(--r-ctl); transition: color 0.12s ease;\n }\n .tabbar a.active { color: var(--orange); }\n .tabbar a svg { display: block; }\n body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }\n .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }\n}\n\n/* Anti-zoom iOS : les champs doivent faire >= 16px\n (filet global : tokens.css force aussi 16 px au doigt) */\n@media (max-width: 900px) {\n .field input, .field select,\n .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }\n}\n\n/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */\n@media (pointer: coarse) {\n .brand { position: relative; }\n .brand::after { content: \"\"; position: absolute; inset: -8px; }\n}\n\n/* ================= Footer ================= */\n.footer { background: var(--navy); color: rgba(255, 255, 255, 0.72); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; }\n.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 32px; color: var(--white); letter-spacing: -0.03em; margin-bottom: 4px; display: flex; align-items: center; }\n.footer .fbrand .logo-ico { margin-right: 13px; }\n.footer .fbrand .ka { color: var(--orange); }\n.footer .fbaseline { color: rgba(255, 255, 255, 0.65); font-size: 14px; margin-bottom: 16px; }\n.footer .fbaseline .hl { color: var(--orange); font-weight: 600; }\n.footer b { color: var(--white); }\n.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }\n.footer .fmono { font-size: 11.5px; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.42); margin-top: 18px; }\n\n/* --- Carte interactive (mission 4) --------------------------------------- */\n.results-tools { display: flex; align-items: center; gap: 14px; }\n.view-toggle {\n display: inline-flex; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); overflow: hidden; background: var(--surface);\n box-shadow: var(--shadow-flat);\n}\n.view-toggle button {\n border: 0; background: transparent; padding: 7px 16px; cursor: pointer;\n font-size: 12.5px; font-weight: 600; color: var(--ink-2);\n}\n/* zone tactile >= 44 px au doigt (coarse pointer = mobile/tablette) */\n@media (pointer: coarse) {\n .view-toggle button { min-height: 44px; padding: 10px 18px; }\n}\n.view-toggle button + button { border-left: 1px solid var(--line); }\n.view-toggle button.on { background: var(--blue); color: var(--white); }\n\n.map-split {\n display: grid; grid-template-columns: minmax(300px, 400px) 1fr; gap: 18px;\n height: calc(100dvh - 170px); min-height: 420px;\n}\n.map-list {\n overflow-y: auto; display: flex; flex-direction: column; gap: 14px;\n padding-right: 4px; scrollbar-width: thin;\n}\n.map-list .card { margin: 0; flex: 0 0 auto; }\n.mapview {\n width: 100%; height: 100%; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.map-loading {\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-3); font-size: 13px;\n}\n\n/* mobile d'abord : carte plein écran, liste masquée (bascule via l'onglet) */\n@media (max-width: 780px) {\n .map-split { grid-template-columns: 1fr; height: calc(100dvh - 210px); }\n .map-list { display: none; }\n .results-tools { width: 100%; justify-content: space-between; }\n}\n\n/* popup MapLibre au style Lou-Ka */\n.maplibregl-popup-content {\n padding: 0; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); box-shadow: var(--shadow-off);\n font-family: var(--font-body);\n}\n.maplibregl-popup-close-button {\n font-size: 18px; padding: 2px 8px; color: var(--white); z-index: 2;\n text-shadow: 0 0 4px rgba(20, 24, 20, 0.8);\n}\n.mv-pop img, .mv-noimg { width: 100%; height: 130px; object-fit: cover; }\n.mv-noimg { display: flex; align-items: center; justify-content: center;\n font-size: 34px; background: var(--grey); }\n.mv-pop-body { padding: 10px 12px 12px; }\n.mv-pop-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--navy); }\n.mv-pop-price small { font-weight: 400; color: var(--ink-3); font-size: 12px; }\n.mv-pop-title { font-size: 13px; color: var(--ink-2); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mv-pop-meta { font-size: 11.5px; color: var(--ink-2); margin-top: 6px; }\n.mv-pop-src { font-size: 10px; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }\n.mv-pop-cta {\n display: block; margin-top: 10px; padding: 9px 10px; text-align: center;\n background: var(--orange); color: var(--white); border-radius: var(--r-pill);\n font-weight: 600; font-size: 13px;\n}\n.mv-pop-cta:hover { background: var(--orange-deep); }\n\n/* --- Commodités de proximité (fiche) -------------------------------------- */\n.poi-list {\n list-style: none; margin: 0; padding: 0;\n display: flex; flex-direction: column; gap: 2px;\n}\n.poi-list li {\n display: flex; align-items: center; gap: 9px;\n padding: 5px 2px; border-bottom: 1px solid var(--line);\n font-size: 13px;\n}\n.poi-list li:last-child { border-bottom: 0; }\n.poi-ico { width: 20px; text-align: center; flex: 0 0 auto; }\n.poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.poi-dist { font-size: 11.5px; color: var(--navy); font-weight: 600; flex: 0 0 auto; }\n\n/* --- Menu déroulant mobile ------------------------------------------------- */\n.menu-btn {\n display: none; position: relative; z-index: 60;\n width: 44px; height: 44px; margin-left: auto;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); cursor: pointer; padding: 0;\n flex-direction: column; align-items: center; justify-content: center; gap: 5px;\n box-shadow: var(--shadow-flat);\n transition: box-shadow 0.15s ease, transform 0.15s ease;\n}\n.menu-btn:active { transform: scale(0.96); }\n.menu-btn span {\n display: block; width: 18px; height: 2px; background: var(--navy);\n border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease;\n}\n.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }\n.menu-btn.open span:nth-child(2) { opacity: 0; }\n.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }\n\n.mobile-menu {\n display: none; position: absolute; left: 0; right: 0; top: 100%;\n background: var(--white); border-bottom: 1px solid var(--line);\n box-shadow: 0 18px 30px rgba(20, 24, 20, 0.14);\n padding: 8px 16px calc(16px + env(safe-area-inset-bottom));\n max-height: 0; overflow: hidden; opacity: 0;\n transition: max-height 0.3s ease, opacity 0.22s ease;\n}\n.mobile-menu { z-index: var(--z-modal, 900); }\n.mobile-menu.open { max-height: 480px; opacity: 1; }\n/* Le drawer vit DANS le header : quand il est ouvert, le header monte au niveau\n modal pour peindre au-dessus du .mm-backdrop (frère du header, overlay 800). */\n.header:has(.mobile-menu.open) { z-index: var(--z-modal, 900); }\n.mm-link {\n display: flex; align-items: center; gap: 12px;\n padding: 15px 10px; min-height: 52px;\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n letter-spacing: -0.02em; border-bottom: 1px solid var(--line);\n color: var(--navy);\n opacity: 0; transform: translateY(-8px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); }\n.mm-link.active { color: var(--orange); }\n.mm-link.active .mm-arrow { opacity: 1; color: var(--orange); }\n.mm-ico { width: 26px; text-align: center; font-size: 17px; }\n.mm-arrow { margin-left: auto; opacity: 0.25; color: var(--orange); transition: opacity 0.15s ease; }\n.mm-link:active { background: var(--orange-soft); border-radius: var(--r-ctl); }\n.mm-foot {\n padding: 12px 10px 4px;\n font-size: 11px; color: var(--ink-3); letter-spacing: 0.02em;\n}\n.mm-backdrop {\n position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px);\n}\n@media (max-width: 760px) {\n .menu-btn { display: flex; }\n .nav { display: none; }\n .mobile-menu { display: block; }\n}\n\n/* --- Bandeau de consentement (témoins) ------------------------------------- */\n.cookie-banner {\n position: fixed; left: 12px; right: 12px;\n bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80;\n animation: cookie-in 0.35s ease;\n}\n@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } }\n.cookie-inner {\n max-width: 720px; margin: 0 auto; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 16px 18px;\n}\n.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }\n.cookie-text b { color: var(--navy); }\n.cookie-text a { text-decoration: underline; text-underline-offset: 2px; }\n.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n.cookie-opt {\n display: flex; align-items: flex-start; gap: 10px; font-size: 13px;\n color: var(--ink-2); cursor: pointer;\n}\n.cookie-opt input { margin-top: 2px; accent-color: var(--orange); width: 16px; height: 16px; }\n.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; }\n@media (max-width: 640px) {\n .cookie-actions { justify-content: stretch; }\n .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; }\n}\n\n/* --- Page confidentialité + lien pied de page ------------------------------- */\n.legal { padding: 48px 0 70px; max-width: 760px; }\n.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; color: var(--navy); }\n.legal section { margin-top: 26px; }\n.legal h3 { font-size: 18px; margin-bottom: 8px; }\n.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }\n.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }\n.flink {\n border: 0; background: none; padding: 0; cursor: pointer;\n color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px;\n}\n\n/* --- Section « Le quartier » (fiche) --------------------------------------- */\n.quartier { margin-top: 34px; }\n.quartier h2 { font-size: 24px; letter-spacing: -0.02em; color: var(--navy); }\n.q-sub { color: var(--ink-3); font-size: 13px; margin: 4px 0 16px; }\n.q-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;\n}\n@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n.q-cell {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 12px 14px; box-shadow: var(--shadow-flat);\n}\n.q-val { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.q-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px; }\n.q-prox { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }\n.q-bar { display: flex; align-items: center; gap: 10px; font-size: 13px; }\n.q-bar-label { flex: 0 0 150px; color: var(--ink-2); }\n@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n.q-bar-track {\n flex: 1; height: 10px; background: var(--grey);\n border-radius: var(--r-pill); overflow: hidden;\n}\n.q-bar-fill { display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill); }\n.q-bar-num { flex: 0 0 30px; text-align: right;\n font-size: 11.5px; font-weight: 700; color: var(--navy); }\n.q-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.q-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 7px 14px; font-size: 12.5px; background: var(--surface);\n}\n.q-badge.cool { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.q-badge.hot { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.quartier .fine { margin-top: 10px; }\n\n/* --- Fiche v2 (mobile-first) ------------------------------------------------ */\n/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka\n « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`\n / `display:contents` pour réordonner — les blocs sans `order` retombaient à\n order:0 et passaient devant la galerie sur mobile. Mobile : colonnes\n empilées ; desktop : grid 2 colonnes (logement | synthèse). */\n.fiche { display: flex; flex-direction: column; gap: 22px; }\n.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n@media (min-width: 900px) {\n .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n .f-col { gap: 26px; }\n}\n.f-bloc { min-width: 0; }\n.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n.f-bloc:empty { display: none; }\n\n/* galerie à balayage natif */\n.carousel { position: relative; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); background: var(--grey); box-shadow: var(--shadow-off-soft); }\n.carousel-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch; scrollbar-width: none; aspect-ratio: 16/11;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track img {\n flex: 0 0 100%; width: 100%; object-fit: cover; scroll-snap-align: center;\n cursor: zoom-in;\n}\n.carousel-empty { display: flex; align-items: center; justify-content: center;\n aspect-ratio: 16/11; font-size: 48px; }\n.carousel-count {\n position: absolute; right: 12px; bottom: 12px; z-index: 2;\n background: rgba(20, 24, 20, 0.78); color: var(--white);\n font-size: 12px; font-weight: 600;\n padding: 4px 11px; border-radius: var(--r-pill);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.carousel-nav {\n position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--ink);\n background: rgba(255, 255, 255, 0.92); color: var(--navy); font-size: 22px; cursor: pointer;\n display: flex; align-items: center; justify-content: center; line-height: 1;\n box-shadow: var(--shadow-flat);\n}\n.carousel-nav.prev { left: 10px; } .carousel-nav.next { right: 10px; }\n@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n\n/* bandeau prix + badge marché */\n.f-hero .price { font-size: clamp(34px, 8vw, 44px); }\n.deal-badge {\n display: inline-flex; align-items: center; gap: 6px; margin: 8px 0 4px;\n border-radius: var(--r-pill); padding: 7px 14px; font-size: 13px; font-weight: 600;\n border: 1.5px solid var(--ink);\n}\n.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.deal-ok { background: var(--surface); color: var(--ink-2); }\n.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.f-hero h1 { font-size: clamp(20px, 4.5vw, 26px); margin-top: 8px; }\n\n/* chips clés à défilement horizontal */\n.chips-scroll {\n display: flex; gap: 8px; overflow-x: auto; padding: 12px 0 6px;\n scrollbar-width: none; -webkit-overflow-scrolling: touch;\n}\n.chips-scroll::-webkit-scrollbar { display: none; }\n.chip-key {\n flex: 0 0 auto; background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 8px 15px;\n font-size: 12px; font-weight: 600; white-space: nowrap; min-height: 34px;\n display: inline-flex; align-items: center;\n}\n\n/* ancres de navigation rapide */\n.ancres {\n display: flex; gap: 4px; overflow-x: auto; padding: 6px 0 2px;\n scrollbar-width: none; border-bottom: 1px solid var(--line); margin-bottom: 8px;\n}\n.ancres::-webkit-scrollbar { display: none; }\n.ancres a {\n flex: 0 0 auto; padding: 9px 14px; font-size: 13.5px; font-weight: 600;\n color: var(--ink-2); border-radius: var(--r-pill); min-height: 44px;\n display: inline-flex; align-items: center;\n}\n.ancres a:hover { background: var(--orange-soft); color: var(--orange); }\nhtml { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */\n\n/* description restructurée */\n.enbref {\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-card); padding: 12px 16px; color: var(--navy);\n font-size: 15px; margin: 0 0 14px;\n}\n.desc-section { margin-bottom: 12px; }\n.desc-section h4 { font-size: 15px; margin-bottom: 3px; }\n.desc-section p { color: var(--ink-2); font-size: 14.5px; margin: 0; white-space: pre-line; }\n.texte-original { margin-top: 12px; }\n.texte-original summary {\n cursor: pointer; font-size: 12.5px;\n color: var(--ink-3); min-height: 44px; display: flex; align-items: center;\n}\n.texte-original p { color: var(--ink-3); font-size: 13px; white-space: pre-line; }\n\n/* inclusions : confirmées vs mentionnées */\n.amenity.confirmed { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.25); color: var(--blue); font-weight: 600; }\n.amenity.unconfirmed { background: transparent; border-style: dashed; border-color: var(--line-strong); color: var(--ink-3); }\n\n/* détails pratiques : historique de prix */\n.prix-histo {\n margin-top: 12px; padding: 10px 14px; border-radius: var(--r-card);\n border: 1.5px solid var(--ink); font-size: 13.5px; background: var(--surface);\n}\n.prix-histo.down { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n\n/* quartier compacté sur mobile */\n@media (max-width: 640px) {\n .q-cell { padding: 9px 11px; }\n .q-val { font-size: 16px; }\n .q-bar { gap: 8px; font-size: 12px; }\n .q-bar-track { height: 8px; }\n}\n.q-badge-sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }\n\n/* POI groupés repliables */\n.poi-groupe { border: 1.5px solid var(--ink); border-radius: var(--r-card);\n margin-bottom: 8px; background: var(--surface); overflow: hidden; }\n.poi-groupe summary {\n display: flex; justify-content: space-between; align-items: center; gap: 10px;\n padding: 12px 14px; cursor: pointer; font-weight: 600; font-size: 14px;\n min-height: 48px; list-style: none;\n}\n.poi-groupe summary::-webkit-details-marker { display: none; }\n.poi-groupe summary::after { content: \"▾\"; color: var(--ink-3); transition: transform 0.15s ease; }\n.poi-groupe[open] summary::after { transform: rotate(180deg); }\n.poi-resume { font-size: 11.5px; color: var(--ink-3); font-weight: 400; }\n.poi-groupe .poi-list { padding: 0 14px 10px; }\n\n/* CTA sticky mobile */\n.cta-sticky {\n position: fixed; left: 0; right: 0; z-index: var(--z-dropdown, 700);\n bottom: var(--consent-h, 0px);\n display: flex; align-items: center; gap: 12px;\n background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(12px);\n border-top: 1px solid var(--line);\n padding: 10px 16px calc(10px + env(safe-area-inset-bottom));\n}\n.cta-sticky .cta { flex: 1; margin: 0; text-align: center; }\n.cta-sticky-prix { font-family: var(--font-display); font-weight: 700; font-size: 19px; white-space: nowrap; color: var(--navy); }\n.cta-sticky-prix small { font-weight: 500; font-size: 10px; color: var(--ink-3); }\n@media (min-width: 900px) { .cta-sticky { display: none; } }\n@media (max-width: 899px) {\n .cta-desktop { display: none; }\n .detail { padding-bottom: 84px; } /* place pour le CTA sticky */\n}\n.f-foot { margin-top: 26px; }\n\n/* --- Passerelle vers l'annonce originale ------------------------------------ */\n.passerelle {\n min-height: calc(100dvh - 120px); display: flex; align-items: center;\n justify-content: center; padding: 24px 16px;\n}\n.pass-card {\n width: 100%; max-width: 520px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 30px 28px; text-align: center;\n}\n.pass-brands {\n display: flex; align-items: center; justify-content: center; gap: 14px;\n margin-bottom: 22px; flex-wrap: wrap;\n}\n.pass-brand {\n font-family: var(--font-display); font-weight: 700; font-size: 24px;\n letter-spacing: -0.03em; display: inline-flex; align-items: center; color: var(--navy);\n}\n.pass-brand .ka { color: var(--orange); }\n.pass-trajet { display: inline-flex; align-items: center; gap: 5px; }\n.pass-dot {\n width: 6px; height: 6px; border-radius: 50%; background: var(--orange);\n opacity: 0.25; animation: pass-dot 1.2s ease infinite;\n}\n.pass-dot:nth-child(2) { animation-delay: 0.2s; }\n.pass-dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes pass-dot { 40% { opacity: 1; transform: translateX(2px); } }\n.pass-fleche { color: var(--orange); font-size: 18px; }\n.pass-gestionnaire {\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n display: inline-flex; align-items: center; gap: 8px;\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-pill); padding: 6px 16px; color: var(--blue);\n}\n.pass-card h1 { font-size: 20px; letter-spacing: -0.02em; }\n.pass-texte { color: var(--ink-2); font-size: 14.5px; margin: 10px 0 20px; }\n.pass-progress {\n height: 10px; border-radius: var(--r-pill);\n overflow: hidden; background: var(--grey); margin-bottom: 20px;\n}\n.pass-progress-fill {\n display: block; height: 100%; width: 0; background: var(--orange);\n border-radius: var(--r-pill);\n}\n.pass-progress-fill.go { animation: pass-fill 2.2s linear forwards; }\n@keyframes pass-fill { to { width: 100%; } }\n@media (prefers-reduced-motion: reduce) {\n .pass-progress-fill.go { animation: none; width: 100%; }\n .pass-dot { animation: none; opacity: 0.7; }\n}\n.pass-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; }\n\n/* --- boutons PDF -------------------------------------------------------- */\n.btn-pdf { display: block; text-align: center; margin-top: 10px; width: 100%; }\n\n/* --- Onglets de la page Stats -------------------------------------------- */\n.onglets {\n display: flex; gap: 8px; overflow-x: auto; margin: 26px 0 22px;\n padding-bottom: 4px; scrollbar-width: none;\n border-bottom: 1px solid var(--line);\n}\n.onglets::-webkit-scrollbar { display: none; }\n.onglet {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;\n border: 1.5px solid var(--ink);\n border-radius: var(--r-pill);\n background: var(--surface); color: var(--ink-2); cursor: pointer;\n padding: 10px 18px; font-weight: 600; font-size: 13.5px; min-height: 44px;\n margin-bottom: 8px;\n transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;\n}\n.onglet:hover { background: var(--blue-soft); color: var(--blue); border-color: var(--blue); }\n.onglet.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* baisses de prix */\n.baisses { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }\n.baisses li {\n display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n padding: 9px 4px; border-bottom: 1px solid var(--line); font-size: 13.5px;\n}\n.baisses li a { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }\n.baisse-ville { color: var(--ink-3); font-size: 11.5px; }\n.baisse-prix { margin-left: auto; }\n.baisse-prix s { color: var(--ink-3); }\n.baisse-pct {\n font-style: normal; font-weight: 700;\n background: var(--blue-soft);\n color: var(--blue); border-radius: var(--r-pill); padding: 2px 9px;\n font-size: 11px; margin-left: 8px;\n}\n.alertes { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n\n/* ================= Pagination (12 annonces/page) ================= */\n.pager {\n display: flex; align-items: center; justify-content: space-between;\n gap: 16px; flex-wrap: wrap; margin: 26px 0 10px; padding-top: 18px;\n border-top: 1px solid var(--line);\n}\n.pager-info {\n font-size: 12.5px; color: var(--ink-2); letter-spacing: 0.02em;\n}\n.pager-info em { font-style: normal; color: var(--ink-3); }\n.pager-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.pager-btn {\n min-width: 38px; height: 38px; padding: 0 10px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); cursor: pointer;\n font-size: 13px; font-weight: 600;\n color: var(--navy); transition: all 0.12s ease;\n}\n.pager-btn:hover:not(:disabled):not(.on) {\n background: var(--blue-soft); border-color: var(--blue); color: var(--blue);\n}\n.pager-btn.on { background: var(--navy); border-color: var(--navy); color: var(--white); cursor: default; }\n.pager-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n.pager-ellipsis { color: var(--ink-3); padding: 0 2px; }\n.map-list .pager { margin: 10px 0 4px; }\n@media (max-width: 640px) {\n .pager { justify-content: center; }\n .pager-info { width: 100%; text-align: center; order: 2; }\n}\n\n/* ================= Icônes SVG inline — alignement ================= */\n.chip-ico { display: inline-flex; align-items: center; gap: 6px; }\n.chip-ico svg { flex: none; }\n.view-toggle button, .seg button { display: inline-flex; align-items: center; gap: 6px; }\n.fab { align-items: center; gap: 8px; }\n.fab svg { flex: none; }\n.mm-ico { display: inline-flex; align-items: center; }\n.notice .big { display: flex; justify-content: center; color: var(--ink-3); }\n.notice .big svg { display: block; }\n.card-img .noimg { color: var(--ink-3); }\n.badge.right { display: inline-flex; align-items: center; gap: 4px; }\n.btn-pdf { display: inline-flex; align-items: center; gap: 7px; }\n.f-search svg { flex: none; color: var(--ink-3); }\n.mv-noimg { display: flex; align-items: center; justify-content: center; color: var(--ink-3); }\n\n/* footer — liens (Groupe KA, courriels, mentions légales) */\n.footer .frow a { color: var(--white); text-decoration: underline; text-decoration-color: rgba(255, 106, 0, 0.5); text-underline-offset: 3px; }\n.footer .frow a:hover { color: var(--orange); }\n.footer .fmono a:hover { color: var(--orange); }\n\n/* ================= Compte (connexion Google) ================= */\n.login-btn {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 8px 16px; margin-left: 10px; min-height: 38px;\n background: var(--navy); border: none;\n border-radius: var(--r-pill); font-weight: 600; font-size: 13.5px; color: var(--white);\n transition: all 0.12s ease; white-space: nowrap;\n box-shadow: 0 4px 14px rgba(20, 24, 20, 0.22);\n}\n.login-btn:hover { background: var(--blue); }\n.account { position: relative; margin-left: 10px; }\n.account-btn {\n width: 38px; height: 38px; padding: 0; border: 2px solid var(--orange);\n border-radius: var(--r-pill); overflow: hidden; cursor: pointer;\n background: var(--orange-soft); display: flex; align-items: center; justify-content: center;\n}\n.account-btn img { width: 100%; height: 100%; object-fit: cover; }\n.account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }\n/* Click-catcher plein écran rendu via createPortal à la racine de \n (un fixed descendant du header blurré se re-parentait → 66 px de haut).\n z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le\n header et doit peindre au-dessus du voile. */\n.account-backdrop { position: fixed; inset: 0; z-index: 40; }\n.account-menu {\n position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;\n min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off); overflow: hidden;\n}\n.account-id { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-bottom: 1px solid var(--line); }\n.account-id b { font-size: 14px; }\n.account-id span { font-size: 11.5px; color: var(--ink-3); word-break: break-all; }\n.account-menu button {\n width: 100%; text-align: left; padding: 11px 14px; border: 0; cursor: pointer;\n background: transparent; font-weight: 600; font-size: 13.5px; color: var(--danger);\n}\n.account-menu button:hover { background: var(--orange-soft); color: var(--navy); }\n@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n\n/* ================= Profil — carte de membre Groupe KA ================= */\n.profil { padding-top: 40px; padding-bottom: 60px; }\n.profil h1 { font-size: clamp(30px, 5vw, 44px); margin: 10px 0 26px; color: var(--navy); }\n.profil .lede { max-width: 520px; color: var(--ink-2); margin-bottom: 22px; }\n\n.pc {\n position: relative; max-width: 520px; overflow: hidden;\n background: linear-gradient(135deg, #101a40 0%, var(--navy) 55%, #080e24 100%);\n color: var(--white);\n border-radius: 20px;\n padding: 24px 26px 0; box-shadow: var(--shadow-off);\n}\n.pc-watermark {\n position: absolute; right: -18px; top: -34px; pointer-events: none;\n font-family: var(--font-display); font-weight: 700; font-size: 170px;\n letter-spacing: -0.06em; color: rgba(255, 106, 0, 0.07);\n transform: rotate(-8deg); line-height: 1;\n}\n.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }\n.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }\n.pc-ka { color: var(--orange); margin-left: 1px; display: inline-block; }\n.pc-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.5); }\n.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }\n.pc-id-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2em; color: rgba(255, 106, 0, 0.75); }\n.pc-id {\n font-weight: 700; color: var(--orange);\n font-size: clamp(22px, 6vw, 34px); letter-spacing: 0.14em;\n text-shadow: 0 0 24px rgba(255, 106, 0, 0.35);\n}\n.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }\n.pc-holder { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pc-holder-name { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pc-holder-since { font-size: 10.5px; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.06em; }\n.pc-role-badge {\n display: inline-block; width: fit-content; margin: 3px 0 2px;\n background: var(--orange); color: var(--white);\n font-size: 9px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.12em;\n padding: 2px 8px 3px; border-radius: var(--r-pill);\n}\n.pc-avatar { width: 52px; height: 52px; border-radius: var(--r-pill); border: 2px solid var(--orange); flex: none; }\n.pc-strip { display: flex; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(255, 106, 0, 0.1); border-top: 1px solid rgba(255, 106, 0, 0.25); overflow: hidden; }\n.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--orange); opacity: 0.7; }\n.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.35; }\n.pc-strip i:nth-child(3n+1) { height: 9px; }\n.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.9; }\n.pc-copy { margin-top: 16px; }\n.pc-copy.ok { background: var(--blue); color: var(--white); }\n\n.profil-grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 12px; margin-top: 30px; max-width: 760px;\n}\n.pg-item {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 14px 16px;\n display: flex; flex-direction: column; gap: 4px; box-shadow: var(--shadow-flat);\n}\n.pg-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }\n.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }\n.pg-value.mono { color: var(--blue); }\n\n.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }\n.profil-note a { text-decoration: underline; text-underline-offset: 3px; }\n.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }\n.profil-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n\n.account-kaid { font-size: 10.5px; color: var(--blue); letter-spacing: 0.08em; }\n.account-link {\n display: block; width: 100%; text-align: left; padding: 11px 14px;\n font-weight: 600; font-size: 13.5px; color: var(--navy);\n border-bottom: 1px solid var(--line);\n}\n.account-link:hover { background: var(--orange-soft); }\n\n/* profil public — section + lien partageable */\n.profil-public { margin-top: 30px; max-width: 640px; }\n.profil-public h3 { font-size: 18px; margin-bottom: 6px; }\n.profil-public p { color: var(--ink-2); font-size: 13.5px; margin: 0 0 12px; }\n.public-link {\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n margin-top: 14px; padding: 12px 14px; background: var(--blue-soft);\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-card);\n}\n.public-link a { font-size: 13px; color: var(--blue); text-decoration: underline; text-underline-offset: 3px; word-break: break-all; }\n\n/* ================= Bienvenue — choix du rôle ================= */\n.bienvenue { padding-top: 44px; padding-bottom: 70px; }\n.bienvenue h1 { font-size: clamp(30px, 5vw, 46px); margin: 10px 0 14px; color: var(--navy); }\n.bienvenue .lede { max-width: 480px; color: var(--ink-2); margin-bottom: 30px; }\n.role-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; max-width: 780px; }\n.role-card {\n display: flex; flex-direction: column; align-items: flex-start; gap: 10px;\n text-align: left; padding: 24px 24px 20px; cursor: pointer;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-flat);\n font-family: var(--font-body); transition: all 0.15s ease;\n}\n.role-card:hover:not(:disabled) { transform: translateY(-4px); box-shadow: var(--shadow-off); border-color: rgba(255, 106, 0, 0.4); }\n.role-card:disabled { opacity: 0.6; cursor: wait; }\n.role-ico { display: flex; width: 54px; height: 54px; align-items: center; justify-content: center; background: var(--orange-soft); color: var(--orange); border-radius: 16px; }\n.role-title { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.role-desc { color: var(--ink-2); font-size: 13.5px; }\n.role-perks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }\n.role-perks li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }\n.role-perks li::before {\n content: \"✓\"; flex: none; width: 16px; height: 16px; border-radius: 50%;\n background: var(--orange); color: var(--white); font-size: 10px; font-weight: 700;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.role-cta { margin-top: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--orange); }\n\n/* ================= Favoris — cœur sur les cartes ================= */\n.fav-btn {\n position: absolute; right: 10px; bottom: 10px; z-index: 2;\n width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;\n background: rgba(255, 255, 255, 0.94); border: none;\n border-radius: var(--r-pill); cursor: pointer; color: var(--navy);\n box-shadow: var(--shadow-flat);\n transition: transform 0.12s ease, background 0.12s ease, color 0.12s ease;\n}\n.fav-btn:hover { transform: scale(1.12); }\n.fav-btn.on { background: var(--white); color: var(--orange); }\n.fav-btn.on svg { filter: drop-shadow(0 0 3px rgba(255, 106, 0, 0.35)); }\n\n/* ================= Espace gestionnaire ================= */\n.claim-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; max-width: 640px; }\n.claim-row select {\n flex: 1; min-width: 260px; padding: 11px 14px; font-size: 14px;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); font-family: var(--font-body);\n}\n.claim-error { color: var(--danger); font-size: 13.5px; font-weight: 600; margin-top: 10px; }\n.org-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 22px; }\n.org-stat { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); }\n.org-form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }\n.org-form h3 { font-size: 18px; }\n.org-form label { display: flex; flex-direction: column; gap: 5px; flex: 1; }\n.org-form label span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }\n.org-form input, .org-form textarea {\n padding: 10px 14px; font-size: 14px; font-family: var(--font-body);\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);\n}\n.org-form textarea { resize: vertical; }\n.org-form input:focus, .org-form textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.org-form-row { display: flex; gap: 12px; flex-wrap: wrap; }\n.org-form .btn { align-self: flex-start; }\n\n/* ================= Page publique /g/{source} ================= */\n.orgpub-head { display: flex; gap: 20px; align-items: flex-start; margin-top: 8px; }\n.orgpub-logo { width: 84px; height: 84px; object-fit: contain; background: var(--surface); border: 1.5px solid var(--ink); border-radius: 18px; padding: 8px; flex: none; box-shadow: var(--shadow-flat); }\n.orgpub-id h1 { font-size: clamp(26px, 4.4vw, 40px); margin: 6px 0 8px; color: var(--navy); }\n.orgpub-tagline { font-size: 16px; color: var(--ink-2); margin: 0 0 12px; max-width: 640px; }\n.orgpub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.orgpub-meta a.stat-chip:hover { background: var(--blue-soft); }\n.orgpub-verified { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); font-weight: 600; }\n.orgpub-desc { max-width: 720px; color: var(--ink-2); margin: 18px 0 6px; white-space: pre-line; }\n.orgpub .results-head h2 { display: inline-flex; align-items: center; gap: 8px; }\n@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n\n/* ================= Éditeur de profil (photo, bio, réseaux) ================= */\n.profil-edit { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--line-strong); }\n/* profil géré au hub Groupe KA — lecture seule */\n.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }\n.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }\n.hub-profile a.stat-chip:hover { background: var(--blue-soft); }\n.hub-profile .btn-primary { display: inline-flex; align-items: center; gap: 7px; }\n.pe-avatar-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.pe-avatar {\n width: 84px; height: 84px; border-radius: var(--r-pill); object-fit: cover;\n border: 2.5px solid var(--orange); box-shadow: var(--shadow-flat);\n display: flex; align-items: center; justify-content: center;\n}\n.pe-avatar-empty { background: var(--orange-soft); color: var(--orange); font-family: var(--font-display); font-weight: 700; font-size: 34px; }\n.pe-avatar-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }\n.pe-avatar-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n.pe-hint { font-size: 11px; color: var(--ink-3); }\n.pe-socials-title { font-size: 15px; margin-top: 6px; }\n.pe-socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }\n.pe-social { display: flex !important; flex-direction: row !important; align-items: center; gap: 0 !important; }\n.pe-social-ico {\n display: flex; align-items: center; justify-content: center;\n width: 40px; align-self: stretch; flex: none;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-ctl) 0 0 var(--r-ctl);\n}\n.pe-social input { flex: 1; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; border-left: 0; }\n\n/* profil public — à propos + réseaux */\n.pub-about { max-width: 640px; margin-top: 22px; }\n.pub-bio { font-size: 15.5px; color: var(--ink); margin: 0 0 12px; white-space: pre-line; }\n.pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.pub-socials { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }\n.pub-social {\n display: flex; align-items: center; justify-content: center;\n width: 44px; height: 44px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: 14px; color: var(--navy);\n transition: all 0.12s ease;\n}\n.pub-social:hover { background: var(--navy); border-color: var(--navy); color: var(--white); transform: translateY(-2px); box-shadow: var(--shadow-off-soft); }\n\n/* -----------------------------------------------------------------------------\n Lou-Ka Maps (framework Ka Maps par Groupe Ka)\n Jetons du design system appliqués au chrome de la carte + sync liste↔carte.\n----------------------------------------------------------------------------- */\n.mapview .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n}\n.mapview { position: relative; overflow: hidden; }\n.mapview .ka-map { position: absolute; inset: 0; }\n\n/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en orange */\n.lmap3d {\n position: relative; height: 340px; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.lmap3d .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n position: absolute; inset: 0;\n}\n.lmap3d-skel {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.lmap3d-legende {\n position: absolute; left: 10px; bottom: 10px; z-index: 5;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 4px 10px; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n font-size: 10.5px; font-weight: 500; color: var(--ink-2);\n pointer-events: none;\n}\n.lmap3d-legende i {\n width: 10px; height: 10px; border-radius: 3px;\n background: var(--orange); border: 1px solid rgba(20, 24, 20, 0.25);\n}\n@media (max-width: 780px) { .lmap3d { height: 280px; } }\n.ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }\n.ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }\n.ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }\n\n/* carte de liste synchronisée avec la carte (survol / sélection) */\n.map-card { border-radius: var(--r-card); }\n.map-card-sel { outline: 2px solid var(--blue); outline-offset: 2px; }\n\n/* =============================================================================\n HARMONISATION « ÉDITORIAL SHARP » (Groupe KA) — couche finale posée après\n les styles historiques : géométrie 10/6 px, bordures encre 1,5 px, ombres\n décalées dures, accent orange Lou-Ka. La carte Ka Maps (Mapbox) conserve\n son propre thème marine/orange à l'intérieur du canevas.\n============================================================================= */\n\n/* — contrôles : plus de pilules (les chips/badges gardent --r-pill) — */\n.nav a, .login-btn, .cta, .mv-pop-cta, .f-search, .f-more, .pager-btn,\n.onglet, .view-toggle, .fab, .f-ctl, .pass-gestionnaire, .hbar-track,\n.q-bar-track, .pass-progress { border-radius: var(--r-ctl); }\n.seg button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); }\n.seg button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }\n\n/* — header : papier translucide + trait d'encre — */\n.header {\n background: rgba(245, 243, 238, 0.92);\n border-bottom: 1.5px solid var(--ink);\n}\n.nav a { font-family: var(--font-display); }\n.nav a:hover { color: var(--accent-deep); background: var(--accent-soft); }\n.nav a.active { background: var(--ink); color: var(--accent); }\n.brand .ka {\n background: var(--accent); color: var(--on-accent);\n border-radius: 6px; padding: 0 6px 2px; margin-left: 2px; transform: rotate(-2deg);\n}\n.header .gk-badge { margin-left: 4px; flex: none; }\n@media (max-width: 1080px) { .header .gk-badge { display: none; } }\n.mm-badge {\n display: flex; align-items: center; min-height: var(--touch);\n padding: 8px 10px 2px;\n}\n\n/* — ticker : encre + mono — */\n.ticker { font-family: var(--font-mono); border-bottom: 1.5px solid var(--ink); }\n\n/* — états sélectionnés : encre + accent (fini le bleu) — */\n.seg button.on, .onglet.on, .view-toggle button.on, .f-more.on {\n background: var(--ink); border-color: var(--ink); color: var(--accent);\n}\n.pager-btn.on { background: var(--ink); border-color: var(--ink); color: var(--accent); }\n\n/* — CTA / FAB : aplat accent, bordure encre, ombre décalée — */\n.cta, .mv-pop-cta, .fab {\n background: var(--accent); color: var(--on-accent);\n border: 1.5px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);\n font-family: var(--font-display); font-weight: 700;\n}\n.cta:hover, .mv-pop-cta:hover, .fab:hover { background: var(--accent-deep); }\n.cta:active, .fab:active { box-shadow: none; }\n.fab { box-shadow: 4px 4px 0 var(--ink), 0 10px 28px rgba(20, 24, 20, 0.25); }\n.login-btn {\n background: var(--ink); color: var(--accent);\n border: 1.5px solid var(--ink); box-shadow: none;\n font-family: var(--font-display); font-weight: 700;\n}\n.login-btn:hover { background: var(--accent-deep); color: var(--on-accent); }\n\n/* — cartes : levée nette diagonale (signature du groupe) — */\n.card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }\n.role-card:hover:not(:disabled) { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); border-color: var(--ink); }\n.badge { border: 1.5px solid var(--ink); border-radius: var(--r-ctl); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.badge.type { background: var(--accent); color: var(--on-accent); }\n.source-tag { font-family: var(--font-mono); }\n\n/* — barre d'onglets mobile : papier + encre, actif = accent orange — */\n@media (max-width: 760px) {\n .tabbar { background: rgba(245, 243, 238, 0.95); border-top: 1.5px solid var(--ink); }\n .tabbar a { font-family: var(--font-display); }\n .tabbar a.active { color: var(--accent); }\n}\n.mobile-menu { background: var(--paper); border-bottom: 1.5px solid var(--ink); }\n\n/* — carte de membre KA : encre plutôt que marine — */\n.pc { background: linear-gradient(135deg, #1d231d 0%, var(--ink) 55%, #0b0e0b 100%); }\n\n/* — micro-étiquettes en mono (système) — */\n.f-ctl > span, .f-group > label, .field label, .tile-k, .pg-label,\n.pc-label, .pc-id-label, .crumbs, .viz-sub { font-family: var(--font-mono); }\n\n/* — page Contact (design system) — */\n.contact-page { padding: 44px 0 90px; }\n.contact-page h1 { margin: 10px 0 8px; }\n.contact-page .lede { color: var(--ink-2); max-width: 640px; margin: 0 0 26px; }\n.contact-grid { display: grid; gap: 16px; grid-template-columns: 1fr; margin: 26px 0; }\n@media (min-width: 768px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }\n.contact-card { padding: 18px 20px; }\n.contact-card .klabel { display: block; margin-bottom: 6px; }\n.contact-card a.mail {\n font-family: var(--font-mono); font-weight: 700; font-size: 13.5px;\n color: var(--accent-deep); word-break: break-all;\n text-decoration: underline; text-underline-offset: 3px;\n display: inline-flex; align-items: center; min-height: 44px;\n}\n.contact-card p { margin: 6px 0 0; color: var(--ink-2); font-size: 13.5px; }\n.contact-note {\n border-left: 3px solid var(--accent); background: var(--surface);\n border-top: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);\n border-bottom: 1.5px solid var(--ink); border-radius: 0 var(--r-card) var(--r-card) 0;\n padding: 14px 18px; max-width: 720px; color: var(--ink-2); font-size: 14px;\n}\n.contact-note b { color: var(--ink); }\n.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.contact-sites a {\n display: inline-flex; align-items: center; min-height: 34px;\n padding: 4px 12px; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); font-family: var(--font-mono); font-size: 11px;\n font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;\n transition: transform 0.12s, box-shadow 0.12s;\n}\n.contact-sites a:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.contact-hub { margin-top: 26px; }\n\n/* — rangée de navigation locale au-dessus du footer commun — */\n.prefooter { border-top: 1.5px solid var(--ink); background: var(--surface-2); padding: 18px 0; }\n.prefooter .container {\n display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px;\n font-family: var(--font-mono); font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n}\n.prefooter a, .prefooter .flink {\n color: var(--ink-2); text-decoration: none; text-transform: uppercase;\n letter-spacing: 0.08em; font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;\n min-height: var(--touch); display: inline-flex; align-items: center;\n}\n.prefooter a:hover, .prefooter .flink:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }\n.prefooter .baseline { width: 100%; font-family: var(--font-body); font-weight: 400; font-size: 13px; color: var(--ink-2); text-transform: none; letter-spacing: 0; }\n.ka-footer { margin-top: 0; }\n@media (max-width: 760px) { .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } }\n\n/* ============ Juste valeur (fair value) — pastilles & analyse de prix ====== */\n.fv-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border-radius: var(--r-pill); padding: 6px 12px;\n font-size: 12.5px; font-weight: 700; border: 1.5px solid var(--ink);\n white-space: nowrap;\n}\n.fv-badge .fv-ico { font-size: 10px; line-height: 1; }\n.fv-badge .fv-pct { margin-left: 4px; }\n.fv-badge.fv-compact { padding: 2px 8px; font-size: 11px; gap: 4px; margin-left: 8px; vertical-align: 2px; }\n/* états (mêmes jetons que les badges marché existants — daltonien-compatible :\n l'icône ▼ ≈ ▲ et le libellé portent l'information, pas la couleur seule) */\n.fv-badge.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.35); color: var(--blue); }\n.fv-badge.deal-ok { background: var(--surface); color: var(--ink-2); }\n.fv-badge.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.4); color: var(--orange-deep); }\n\n.f-fairvalue .fv-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 12px; flex-wrap: wrap; margin-bottom: 6px;\n}\n.f-fairvalue .fv-value { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--navy); }\n.f-fairvalue .fv-value small { font-size: 12px; font-weight: 500; color: var(--ink-3); }\n.f-fairvalue .fv-range { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.fv-histo { display: block; width: 100%; height: auto; margin: 10px 0 4px; }\n.fv-histo-lbl { font: 700 10px Inter, system-ui, sans-serif; fill: var(--ink); }\n.fv-histo-lbl-fv { fill: var(--accent-deep, #cc5500); }\n.fv-histo-axis { font: 500 9px Inter, system-ui, sans-serif; fill: var(--ink-3); }\n.fv-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12.5px; color: var(--ink-2); margin: 6px 0 4px; }\n.fv-demo { display: flex; flex-wrap: wrap; gap: 8px; }\n.chip.chip-deal.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* =============================================================================\n MapSearch — recherche synchronisée liste ↔ carte (search/MapSearch.tsx)\n Une seule vérité partagée : compteur, tri, survols et sélections miroirs.\n============================================================================= */\n\n.ms { position: relative; }\n.ms-list { position: relative; scroll-behavior: smooth; }\n\n.ms-list-head {\n display: flex; align-items: center; justify-content: space-between;\n gap: 10px; flex-wrap: wrap; padding: 2px 2px 4px;\n}\n.ms-count { font-size: 13.5px; color: var(--ink-2); }\n.ms-count b { color: var(--ink); font-size: 15px; }\n.ms-nopos {\n margin-left: 8px; font-size: 11px; color: var(--ink-3);\n border: 1px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 8px; cursor: help;\n}\n.ms-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }\n.ms-sort select {\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); padding: 6px 8px; font-size: 12.5px;\n font-weight: 600; color: var(--ink);\n}\n\n.ms-pills { margin: 0 0 4px; }\n\n.ms-refit {\n align-self: flex-start; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--ink-2);\n cursor: pointer; box-shadow: var(--shadow-off-soft);\n}\n.ms-refit:hover { background: var(--navy); color: var(--white); border-color: var(--navy); }\n\n/* barre de progression fine — jamais d'écran qui clignote */\n.ms-progress {\n position: sticky; top: 0; z-index: 4; height: 3px; border-radius: 2px;\n background: linear-gradient(90deg, transparent, var(--accent), transparent);\n background-size: 200% 100%; animation: ms-progress 1s linear infinite;\n}\n@keyframes ms-progress { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n.ms-empty {\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-card);\n padding: 26px 18px; text-align: center; color: var(--ink-2);\n}\n.ms-empty .big { margin-bottom: 6px; color: var(--ink-3); }\n.ms-empty h3 { margin: 0 0 6px; font-size: 16px; color: var(--ink); }\n.ms-empty p { margin: 0 0 14px; font-size: 13px; }\n.ms-empty-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n/* miroirs survol / sélection venus de la carte */\n.map-card { transition: transform 0.15s ease, box-shadow 0.15s ease; }\n.map-card-hover {\n outline: 2px solid var(--accent); outline-offset: 2px;\n transform: translateY(-1px);\n}\n.map-card-pulse { animation: ms-pulse 0.9s ease 1; }\n@keyframes ms-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* indicateur discret : l'annonce survolée sur la carte est hors écran */\n.ms-hover-hint {\n position: sticky; z-index: 5; align-self: center;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 5px 12px;\n font-size: 11.5px; font-weight: 600; pointer-events: none;\n box-shadow: var(--shadow-off-soft);\n}\n.ms-hover-hint.haut { top: 8px; order: -1; }\n.ms-hover-hint.bas { bottom: 8px; }\n\n/* ---------------- carrousel mobile synchronisé aux marqueurs ------------- */\n.ms-carousel {\n position: absolute; left: 0; right: 0; bottom: 10px; z-index: 12;\n display: flex; gap: 10px; overflow-x: auto; padding: 4px 12px;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n.ms-carousel::-webkit-scrollbar { display: none; }\n.ms-car-item {\n flex: 0 0 78%; max-width: 340px; scroll-snap-align: center;\n display: grid; grid-template-columns: 108px 1fr; gap: 0;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); overflow: hidden;\n box-shadow: var(--shadow-off-soft); text-decoration: none; color: inherit;\n}\n.ms-car-item.sel { outline: 2.5px solid var(--accent); outline-offset: 1px; }\n.ms-car-item img, .ms-car-item .smart-img, .ms-car-item .card-fallback {\n width: 108px; height: 96px; object-fit: cover; display: block;\n}\n.ms-car-body { padding: 9px 12px; min-width: 0; }\n.ms-car-price { font-size: 14.5px; font-weight: 700; color: var(--ink); display: flex; gap: 6px; align-items: center; }\n.ms-car-title {\n font-size: 12px; color: var(--ink-2); margin-top: 2px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.ms-car-meta { font-size: 11px; color: var(--ink-3); margin-top: 3px; }\n.ms-car-more {\n flex: 0 0 auto; scroll-snap-align: center; align-self: stretch;\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n background: var(--navy); color: var(--white);\n font-size: 12.5px; font-weight: 600; padding: 0 18px; cursor: pointer;\n}\n\n/* mobile : carte plein cadre, liste masquée, carrousel par-dessus la carte */\n@media (max-width: 780px) {\n .ms { height: calc(100dvh - 190px); }\n .ms-list { display: none; }\n .ms .mapview { height: 100%; }\n .ka-search-area { top: 8px; }\n}\n@media (min-width: 781px) {\n .ms-carousel { display: none; }\n}\n\n/* mobile : réorganisation du chrome carte autour du carrousel */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 56px; left: 10px; bottom: auto; }\n .ms .ka-loading { bottom: 132px; }\n .ms .ka-empty { transform: translate(-50%, -70%); }\n}\n/* l'auto-défilement vers la carte laisse l'entête de résultats visible */\n.ms { scroll-margin-top: 108px; }\n\n/* mobile, vue carte : chrome sans chevauchement (case en haut, outils sous\n elle, FAB Filtres au-dessus du carrousel) */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 88px; left: 10px; bottom: auto; }\n .ms .ka-locate { top: 88px; right: 10px; }\n .fab-carte { bottom: 148px; }\n}\n\n/* =============================================================================\n KA Scores — pastilles, jauges circulaires, page méthodologie\n============================================================================= */\n.ka-badge {\n display: inline-flex; align-items: center; gap: 4px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 1px 8px 1px 3px; font-size: 11px; font-weight: 700;\n color: var(--ink); background: var(--surface); line-height: 1.5;\n vertical-align: middle;\n}\n.ka-badge-logo {\n background: var(--ink); color: var(--white); border-radius: 999px;\n font-size: 8.5px; font-weight: 800; padding: 1.5px 5px; letter-spacing: 0.4px;\n}\n.ka-badge.ka-haut { border-color: var(--green, #256d43); }\n.ka-badge.ka-haut .ka-badge-logo { background: var(--green, #256d43); }\n.ka-badge.ka-bon { border-color: var(--accent); }\n.ka-badge.ka-bon .ka-badge-logo { background: var(--accent); }\n.ka-badge.ka-moyen { opacity: 0.9; }\n.ka-badge.ka-bas { opacity: 0.75; }\n\n.ka-circles, .ka-demo-circles {\n display: flex; gap: 18px; flex-wrap: wrap; margin: 14px 0 6px;\n}\n.ka-circle { text-align: center; width: 92px; }\n.ka-circle-fond {\n fill: none; stroke: var(--line); stroke-width: 6;\n}\n.ka-circle-arc {\n fill: none; stroke: var(--ink-3); stroke-width: 6; stroke-linecap: round;\n transition: stroke-dasharray 0.6s ease;\n}\n.ka-circle.ka-haut .ka-circle-arc { stroke: var(--green, #256d43); }\n.ka-circle.ka-bon .ka-circle-arc { stroke: var(--accent); }\n.ka-circle.ka-moyen .ka-circle-arc { stroke: var(--orange, #d97a2b); }\n.ka-circle.ka-bas .ka-circle-arc { stroke: #b3543f; }\n.ka-circle-val { font: 700 17px var(--font-body); fill: var(--ink); }\n.ka-circle-nom { font-size: 12px; font-weight: 700; color: var(--ink); margin-top: 4px; }\n.ka-circle-label { font-size: 10.5px; color: var(--ink-3); }\n\n.f-kascores h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ka-perso {\n font-size: 12px; font-weight: 600; color: var(--ink-2);\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 10px;\n}\n.ka-detail summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n padding: 6px 0;\n}\n.ka-detail-grille {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px; padding: 8px 0 4px;\n}\n.ka-detail-grille h4 { margin: 0 0 4px; font-size: 12px; color: var(--ink); }\n.ka-detail-grille ul { margin: 0 0 10px; padding-left: 16px; font-size: 12.5px; color: var(--ink-2); }\n.ka-detail-grille li { margin: 2px 0; }\n\n.ka-poids { display: grid; gap: 10px; margin: 14px 0; max-width: 460px; }\n.ka-poids-row {\n display: grid; grid-template-columns: 90px 1fr 48px; align-items: center;\n gap: 12px; font-size: 13px; font-weight: 600; color: var(--ink);\n}\n.ka-poids-row input[type=\"range\"] { accent-color: var(--accent); }\n.ka-poids-row b { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ms-car-price .ka-badge { margin-left: 2px; }\n@media (max-width: 780px) {\n .ms .ka-3d { top: 148px; right: 10px; }\n}\n\n\n/* ---- Registre des loyers (fiche) — bloc citoyen registre-des-loyers.ca ---- */\n.f-rdl .rdl-resume { margin: 2px 0 6px; }\n.f-rdl .rdl-ecart { margin: 0 0 8px; }\n.rdl-rooms { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 10px; }\n.rdl-room { font-size: 13px; color: var(--ink-2); white-space: nowrap; }\n.rdl-room i { font-style: normal; color: var(--ink-3); }\n.rdl-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.rdl-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line, #e6e4df);\n vertical-align: top; }\n.rdl-table .rdl-addr { max-width: 46%; }\n.rdl-table .rdl-prix { font-weight: 600; white-space: nowrap; }\n.rdl-table .rdl-date { white-space: nowrap; color: var(--ink-2); }\n.rdl-table .rdl-dist { color: var(--ink-3); white-space: nowrap; text-align: right; }\n.rdl-room-on { background: var(--sand, #f4efe7); border-radius: 6px; padding: 2px 8px; }\n\n\n/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */\n.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }\n.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n font-size: 12.5px; font-weight: 700; }\n.zi-eleve { background: #fde8e8; color: #a12622; }\n.zi-modere { background: #fdf3e0; color: #8a5a00; }\n.zi-present { background: #fdf3e0; color: #8a5a00; }\n.zi-ok { background: #e7f4ea; color: #1e6b34; }\n.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n\n\n/* ---- Historique TAL (fiche) — décisions SOQUIJ à l'adresse ---- */\n.f-tal .tal-pending, .f-tal .tal-ok, .f-tal .tal-resume { margin: 2px 0 8px; }\n.tal-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.tal-liste li { padding: 8px 0; border-top: 1px solid var(--line, #e6e4df); }\n.tal-ligne { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }\n.tal-date { font-weight: 600; color: var(--ink-2); }\n.tal-part { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: var(--sand, #f0ede8); color: var(--ink-2); }\n.tal-part-locateur { background: #fdf3e0; color: #8a5a00; }\n.tal-verdict { font-size: 12px; color: var(--ink-3); }\n.tal-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 5px 0 3px; }\n.tal-tag { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: #fde8e8; color: #a12622; }\n.tal-ref { font-size: 12.5px; color: var(--ink-3); }\n\n\n/* ---- Registre des loyers v2 : tuiles, bande de position, barres ---- */\n.rdl-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 12px; }\n.rdl-kpi { flex: 1 1 90px; min-width: 90px; background: var(--surface-2, #f7f5f1);\n border-radius: 10px; padding: 10px 12px; }\n.rdl-kpi-v { display: block; font-size: 20px; font-weight: 700;\n letter-spacing: -0.02em; color: var(--navy); }\n.rdl-kpi-l { display: block; font-size: 11px; line-height: 1.35;\n color: var(--ink-3); margin-top: 2px; }\n.rdl-strip { width: 100%; height: auto; display: block; margin: 2px 0 4px; }\n.rdl-strip-track { fill: var(--surface-2, #f0ede8); }\n.rdl-strip-box { fill: var(--accent-soft, #fff1e6);\n stroke: var(--accent, #ff6a00); stroke-width: 1; }\n.rdl-strip-med { stroke: var(--ink-2, #4c4a45); stroke-width: 2; }\n.rdl-strip-me { stroke: var(--accent-deep, #cc5500); stroke-width: 2; }\n.rdl-strip-me-dot { fill: var(--accent-deep, #cc5500);\n stroke: var(--surface, #fff); stroke-width: 2; }\n.rdl-strip-lbl, .rdl-strip-axis { font-size: 10.5px; fill: var(--ink-3); }\n.rdl-strip-me-lbl { font-size: 11px; font-weight: 700;\n fill: var(--accent-deep, #cc5500); }\n.rdl-bars { width: 100%; height: auto; display: block; margin: 6px 0 2px; }\n.rdl-bar { fill: var(--accent, #ff6a00); opacity: 0.55; }\n.rdl-bar-on { fill: var(--accent-deep, #cc5500); opacity: 1; }\n.rdl-bar-val { font-size: 10.5px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat { font-size: 11px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat-on { font-weight: 700; fill: var(--ink, #1c1b18); }\n.rdl-bar-n { font-size: 9.5px; fill: var(--ink-3, #8a877f); }\n.rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }\n.rdl-ecart { margin: 4px 0 8px; }\n.rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }\n\n\n/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */\n.q-crime { width: 100%; }\n.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }\n.q-crime-cats li { display: flex; align-items: center; gap: 8px;\n padding: 3px 0; font-size: 12.5px; }\n.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.q-crime-barre i { display: block; height: 100%; border-radius: 5px;\n background: var(--accent, #ff6a00); opacity: 0.6; }\n.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; }\n.q-crime-n small { font-weight: 400; color: var(--ink-3); }\n.q-crime-src { margin-top: 8px; }\n\n\n/* ---- Qualité de l'air (RSQAQ) + Essence à proximité ---- */\n.air-liste { list-style: none; margin: 10px 0 0; padding: 0; }\n.air-liste li { display: flex; align-items: center; gap: 8px;\n padding: 4px 0; font-size: 12.5px; }\n.air-nom { flex: 0 0 44%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.air-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.air-barre i { display: block; height: 100%; border-radius: 5px;\n background: #4d9e64; }\n.air-barre i.air-sur { background: #c96a1f; }\n.air-val { flex: 0 0 34%; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.air-val small { font-weight: 400; color: var(--ink-3); }\n.air-val .air-ref { display: block; font-size: 10px; }\n.gaz-head th { text-align: left; font-size: 11px; color: var(--ink-3);\n font-weight: 600; padding: 2px 8px 2px 0; }\n.gaz-best { display: inline-block; margin-left: 6px; padding: 1px 7px;\n border-radius: 999px; background: #e7f4ea; color: #1e6b34;\n font-size: 10.5px; font-weight: 700; }\n.gaz-adr { display: block; font-size: 11px; color: var(--ink-3); }\n\n\n/* ---- Commerces et transport (fiche) ---- */\n.cm-grille { list-style: none; margin: 6px 0 0; padding: 0;\n display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }\n@media (max-width: 560px) { .cm-grille { grid-template-columns: 1fr; } }\n.cm-item { display: flex; align-items: center; gap: 9px; padding: 5px 0;\n border-bottom: 1px solid var(--line, #eeece7); min-width: 0; }\n.cm-ico { flex: 0 0 28px; }\n.cm-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.cm-nom { font-size: 13px; font-weight: 600; color: var(--ink, #222);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-poi { font-size: 11px; color: var(--ink-3, #8a877f);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-dist { flex: 0 0 auto; font-size: 13px; font-weight: 700;\n color: var(--ink-2, #4c4a45); white-space: nowrap; }\n\n\n/* ---- Coût d'électricité (Hydro-Québec) ---- */\n.hydro-intro { margin: 2px 0 12px; color: var(--ink-2, #4c4a45); }\n.hydro-btn { appearance: none; border: 0; border-radius: 10px; cursor: pointer;\n background: var(--accent, #2b7a3b); color: #fff; font-weight: 700;\n font-size: 14px; padding: 11px 18px; }\n.hydro-btn:disabled { opacity: 0.6; cursor: progress; }\n.hydro-montant { font-size: 26px; font-weight: 800; letter-spacing: -0.02em;\n color: var(--navy, #1c1b18); display: flex; align-items: baseline;\n gap: 10px; flex-wrap: wrap; }\n.hydro-montant small { font-size: 14px; font-weight: 500; color: var(--ink-3); }\n.hydro-an { font-size: 14px; font-weight: 600; color: var(--ink-2, #4c4a45); }\n.hydro-vide { color: var(--ink-3, #8a877f); }\n\n\n/* ============================================================================\n PASSE DE DESIGN v3 — « éditorial sharp » appliqué à toute la fiche\n Blocs = cartes encre (bordure 1,5 px + ombre décalée douce), titres à\n marqueur accent + filet, tuiles KPI blanches à ombre dure, tableaux zébrés,\n barres en dégradé, pastilles unifiées (mono, point d'état, liseré).\n Aucune propriété `order` (règle Groupe Ka : ordre DOM = ordre visuel).\n ========================================================================== */\n\n/* --- Blocs de la fiche : cartes posées sur le papier --- */\n.f-bloc:not(.f-galerie):not(.f-hero) {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-card);\n padding: 18px 20px 16px;\n box-shadow: var(--shadow-off-soft);\n}\n@media (max-width: 640px) {\n .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n}\n\n/* --- Titres de section : carré accent + filet éditorial --- */\n.f-bloc h2, .quartier h2 {\n display: flex; align-items: center; gap: 10px;\n font-size: 19px; letter-spacing: -0.015em; margin-bottom: 14px;\n}\n.f-bloc h2::before, .quartier h2::before {\n content: \"\"; flex: 0 0 9px; width: 9px; height: 9px;\n background: var(--accent); border: 1.5px solid var(--ink);\n box-shadow: 2px 2px 0 var(--ink);\n}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--line);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n","structuredPatch":[{"oldStart":2137,"oldLines":6,"newStart":2137,"newLines":8,"lines":[" .cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }"," .cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }"," .cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }","+.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }","+.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }"," .cr-poste { color: var(--ink-2); }"," .cr-montant {"," text-align: right; font-family: var(--font-mono); font-weight: 600;"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMd4hZN8aer28XBLFco","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Sg8oXP3tCGrvsY9F2UzsdF","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","old_string":".hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--line);\n display: flex; flex-direction: column; gap: 8px;\n}","new_string":".hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--ink);\n display: flex; flex-direction: column; gap: 8px;\n}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2672,"cache_read_input_tokens":104805,"cache_creation":{"ephemeral_5m_input_tokens":2672,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7acf974a-677d-400e-9d46-a2d3903c134e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Sg8oXP3tCGrvsY9F2UzsdF","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9544d65a-74bb-4684-8101-ff2501763211","timestamp":"2026-08-24T22:24:14.693Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","oldString":".hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--line);\n display: flex; flex-direction: column; gap: 8px;\n}","newString":".hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--ink);\n display: flex; flex-direction: column; gap: 8px;\n}","originalFile":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }\n\n/* Garde-fou : jamais de défilement horizontal de la page, même transitoire\n pendant le chargement (bandeau défilant, polices) — clip ne casse pas\n position: sticky, contrairement à hidden. */\nhtml, body { overflow-x: clip; }\n\n/* ================= Header ================= */\n.header {\n position: sticky; top: 0; z-index: var(--z-header, 500);\n background: var(--paper);\n border-bottom: 1px solid var(--line);\n}\n.header-inner { display: flex; align-items: center; gap: 20px; height: 64px; }\n.brand {\n font-family: var(--font-display); font-weight: 700; font-size: 25px;\n letter-spacing: -0.03em; display: flex; align-items: center; line-height: 1;\n color: var(--navy);\n}\n.brand .logo-ico { margin-right: 12px; flex: none; } /* ≈ 40 % de la largeur de l'icône */\n.brand .ka { color: var(--orange); }\n.brand-tag {\n font-size: 11px; font-weight: 500; color: var(--ink-3);\n letter-spacing: 0.02em; margin-left: 12px;\n}\n@media (max-width: 900px) { .brand-tag { display: none; } }\n.nav { margin-left: auto; display: flex; gap: 4px; }\n.nav a {\n padding: 9px 16px; border-radius: var(--r-pill); font-weight: 600; font-size: 14px;\n color: var(--navy); transition: all 0.15s ease; min-height: 40px;\n display: inline-flex; align-items: center;\n}\n.nav a:hover { color: var(--orange); background: var(--orange-soft); }\n.nav a.active { background: var(--navy); color: var(--white); }\n\n/* ================= Ticker ================= */\n.ticker {\n background: var(--navy); color: rgba(255, 255, 255, 0.85); overflow: hidden;\n font-size: 11.5px; font-weight: 500; letter-spacing: 0.08em;\n text-transform: uppercase; padding: 7px 0; white-space: nowrap;\n}\n.ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }\n.ticker span { padding: 0 26px; position: relative; }\n.ticker span::after { content: \"◆\"; position: absolute; right: -6px; color: var(--orange); opacity: 0.8; font-size: 8px; top: 3px; }\n@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) {\n .ticker-track { animation: none; }\n * { transition-duration: 0.01ms !important; }\n}\n\n/* ================= Hero ================= */\n.hero { padding: 58px 0 22px; }\n@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n.hero h1 {\n font-size: clamp(36px, 5.4vw, 56px); font-weight: 700; line-height: 1.06;\n letter-spacing: -0.03em; max-width: 900px; margin-top: 14px; color: var(--navy);\n}\n.hero h1 .outline { color: var(--navy); }\n.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }\n.stat-chip {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 8px 16px; font-size: 12.5px; font-weight: 500;\n color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n box-shadow: var(--shadow-flat);\n}\n.stat-chip b { color: var(--navy); font-weight: 700; }\n.stat-chip .pulse {\n width: 8px; height: 8px; border-radius: 50%; background: var(--orange);\n box-shadow: 0 0 0 4px var(--orange-soft); animation: pulse 2.4s ease infinite;\n}\n@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 106, 0, 0.18); } }\n\n/* ================= Rangée d'avantages ================= */\n.benefits {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px; margin-top: 30px;\n}\n.benefit { display: flex; align-items: center; gap: 12px; }\n.benefit-ico {\n width: 42px; height: 42px; border-radius: var(--r-pill); flex: none;\n display: flex; align-items: center; justify-content: center;\n background: var(--orange-soft); color: var(--orange);\n}\n.benefit-ico.blue { background: var(--blue-soft); color: var(--blue); }\n.benefit-label {\n font-size: 11.5px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--navy);\n}\n@media (max-width: 640px) {\n .benefits { grid-template-columns: 1fr 1fr; gap: 12px; }\n}\n\n/* ================= Filter bar ================= */\n.filterbar {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 14px; margin: 30px 0 6px;\n display: flex; flex-direction: column; gap: 0; min-width: 0;\n}\n\n/* — rangée principale — */\n.f-primary { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; min-width: 0; }\n.f-search {\n flex: 1 1 220px; min-width: 0; max-width: 440px; align-self: center;\n display: flex; align-items: center; gap: 8px;\n border: 1px solid transparent; border-radius: var(--r-pill); background: var(--grey);\n padding: 0 15px; min-height: 42px; color: var(--ink-2);\n transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;\n}\n.f-search:focus-within {\n background: var(--white); border-color: var(--blue);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n.f-search input {\n border: none; background: none; outline: none; flex: 1; min-width: 0;\n font-size: 14px; color: var(--ink); font-family: inherit;\n}\n.f-clear {\n border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n width: 20px; height: 20px; font-size: 10px; cursor: pointer; flex: none;\n display: grid; place-items: center;\n}\n.f-ctl {\n flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center;\n gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n padding: 7px 14px 6px; min-height: 52px; cursor: pointer;\n transition: box-shadow 0.15s ease, border-color 0.15s ease;\n}\n.f-ctl:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.f-ctl > span {\n font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-ctl select {\n border: none; background: transparent; outline: none; font-family: var(--font-display);\n font-weight: 700; font-size: 14.5px; color: var(--navy); cursor: pointer;\n appearance: none; -webkit-appearance: none; padding-right: 16px; min-width: 0; max-width: 170px;\n text-overflow: ellipsis;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right center;\n}\n.range-pair { display: flex; align-items: center; gap: 4px; }\n.range-pair select { max-width: 86px; }\n.range-sep { color: var(--ink-3); font-size: 11px; }\n.f-more {\n flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n transition: all 0.13s ease;\n}\n.f-more:hover { background: var(--blue-soft); border-color: var(--blue); color: var(--blue); }\n.f-more.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* — panneau avancé — */\n.f-adv {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 22px;\n border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n animation: adv-in 0.18s ease;\n}\n@keyframes adv-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }\n.f-group { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.f-group > label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-group-end { justify-content: flex-end; }\n.f-group .btn:disabled { opacity: 0.4; cursor: default; }\n.f-native {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 10px 30px 10px 14px; font-size: 14px; color: var(--ink); outline: none;\n font-family: inherit; min-height: 42px; width: 100%; min-width: 0;\n appearance: none; -webkit-appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center;\n}\n.f-native:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); background: var(--white); }\n\n/* segments (pilules soudées) */\n.seg { display: inline-flex; flex-wrap: wrap; row-gap: 6px; }\n.seg button {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink-2);\n padding: 8px 14px; font-family: var(--font-display); font-weight: 600; font-size: 13px;\n cursor: pointer; margin-left: -1px; white-space: nowrap; min-height: 38px;\n transition: all 0.12s ease;\n}\n.seg button:first-child { border-radius: var(--r-pill) 0 0 var(--r-pill); margin-left: 0; padding-left: 16px; }\n.seg button:last-child { border-radius: 0 var(--r-pill) var(--r-pill) 0; padding-right: 16px; }\n.seg button:hover { background: var(--blue-soft); color: var(--blue); }\n.seg button.on { background: var(--blue); border-color: var(--blue); color: var(--white); position: relative; z-index: 1; }\n\n/* pastilles de filtres actifs */\n.pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }\n.pill {\n display: inline-flex; align-items: center; gap: 7px;\n border: 1px solid rgba(28, 92, 65, 0.3); background: var(--blue-soft); color: var(--blue);\n border-radius: var(--r-pill); padding: 6px 14px; font-size: 12.5px; font-weight: 600;\n cursor: pointer; transition: all 0.13s ease;\n}\n.pill:hover { background: var(--blue); color: var(--white); }\n.pill-x { font-size: 10px; opacity: 0.65; }\n.pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }\n.pill-clear:hover { background: var(--danger); color: #fff; }\n.chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 4px 4px; flex: none; }\n.link-btn {\n background: none; border: none; padding: 0; color: var(--blue); font: inherit;\n text-decoration: underline; cursor: pointer;\n}\n.field { display: flex; flex-direction: column; gap: 5px; }\n.field label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); padding-left: 2px;\n}\n.field input, .field select {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 11px 14px; font-size: 15px; color: var(--ink); outline: none; font-family: inherit;\n transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; min-height: 44px;\n appearance: none; -webkit-appearance: none;\n}\n.field select {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px;\n}\n.field input:focus, .field select:focus {\n border-color: var(--blue); background: var(--white);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n/* .btn / .btn-primary / .btn-ghost : fournis par tokens.css (ka-ui). */\n\n/* ================= Chips ================= */\n.chips { display: flex; gap: 8px; margin: 16px 0 4px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }\n.chips::-webkit-scrollbar { display: none; }\n.chip {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink);\n border-radius: var(--r-pill); padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer;\n font-family: var(--font-display); white-space: nowrap; min-height: 40px;\n transition: transform 0.13s, box-shadow 0.13s, background 0.13s, color 0.13s;\n}\n.chip:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.chip.on { background: var(--ink); border-color: var(--ink); color: var(--accent); box-shadow: 3px 3px 0 var(--accent); }\n\n/* ================= Results ================= */\n.results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }\n.results-head h2 { font-size: 22px; letter-spacing: -0.02em; color: var(--navy); }\n.results-head span { color: var(--ink-3); font-size: 12.5px; font-weight: 500; }\n.grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));\n gap: 22px; padding-bottom: 70px;\n}\n@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n\n.card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-flat);\n transition: transform 0.16s ease, box-shadow 0.16s ease;\n}\n.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-off); }\n.card:focus-visible { outline: 3px solid rgba(28, 92, 65, 0.4); outline-offset: 2px; }\n.card-img { position: relative; aspect-ratio: 16/10.5; background: var(--grey); overflow: hidden; }\n.card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }\n.card:hover .card-img img { transform: scale(1.05); }\n.card-img .noimg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--ink-3); font-size: 34px; }\n.badge {\n position: absolute; top: 12px; left: 12px; border-radius: var(--r-pill); padding: 4px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: 0.04em;\n background: rgba(255, 255, 255, 0.94); color: var(--navy);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.badge.type { background: var(--orange); color: var(--white); }\n.badge.right { left: auto; right: 12px; background: rgba(255, 255, 255, 0.92); }\n.card-body { padding: 16px 18px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\n.card-price { font-family: var(--font-display); font-weight: 700; font-size: 21px; letter-spacing: -0.02em; color: var(--navy); }\n.card-price small { font-weight: 500; color: var(--ink-3); font-size: 11.5px; }\n.card-title { font-weight: 600; font-size: 14.5px; color: var(--ink); }\n.card-meta { color: var(--ink-3); font-size: 12.5px; display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }\n.card-meta .sep { width: 4px; height: 4px; background: var(--orange); border-radius: 50%; }\n.card-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.source-tag {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--blue); background: var(--blue-soft);\n border-radius: var(--r-pill); padding: 3px 10px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;\n}\n.avail { font-size: 11.5px; color: var(--ink-2); font-weight: 500; text-align: right; }\n\n/* ================= Skeletons ================= */\n@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }\n.skel { border-radius: var(--r-card); border: 1.5px solid var(--ink); overflow: hidden; background: var(--surface); }\n.skel .sk-img, .skel .sk-line {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.skel .sk-img { aspect-ratio: 16/10.5; }\n.skel .sk-line { height: 14px; border-radius: 4px; margin: 12px 16px; }\n.skel .sk-line.short { width: 45%; }\n\n/* ================= Empty / error ================= */\n.notice { text-align: center; padding: 72px 24px; color: var(--ink-2); }\n.notice .big { font-size: 44px; margin-bottom: 10px; }\n.notice h2 { color: var(--navy); }\n\n/* ================= Detail page ================= */\n.detail { padding: 30px 0 90px; }\n.crumbs {\n font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;\n color: var(--ink-3); margin-bottom: 20px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;\n}\n.crumbs a { border-bottom: 1.5px solid transparent; }\n.crumbs a:hover { color: var(--blue); border-color: var(--blue); }\n.detail-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n/* les enfants de grille ont min-width:auto par défaut : un contenu large\n (grille du quartier) déborderait de l'écran mobile sans ceci */\n.detail-grid > * { min-width: 0; }\n\n.gallery { display: flex; flex-direction: column; gap: 10px; }\n.gallery-main {\n border-radius: var(--r-card); overflow: hidden; border: 1.5px solid var(--ink);\n aspect-ratio: 16/10; background: var(--grey); cursor: zoom-in; box-shadow: var(--shadow-off-soft);\n}\n.gallery-main img { width: 100%; height: 100%; object-fit: cover; }\n.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; }\n.thumbs button {\n border: 2px solid var(--line); border-radius: 12px; overflow: hidden; padding: 0; cursor: pointer;\n aspect-ratio: 4/3; background: var(--grey); transition: border-color 0.12s ease, transform 0.12s ease;\n}\n.thumbs button:hover { transform: translateY(-2px); }\n.thumbs button.on { border-color: var(--orange); }\n.thumbs img { width: 100%; height: 100%; object-fit: cover; }\n\n.panel {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px; position: sticky; top: 120px;\n}\n@media (max-width: 900px) { .panel { position: static; } }\n.panel .price { font-family: var(--font-display); font-size: 34px; font-weight: 700; letter-spacing: -0.03em; color: var(--navy); }\n.panel .price small { font-size: 12.5px; color: var(--ink-3); font-weight: 500; }\n.panel h1 { font-size: 22px; margin: 8px 0 2px; }\n.panel .loc { color: var(--ink-2); font-size: 14px; }\n.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n/* .klabel : fourni par tokens.css (ka-ui). */\n.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n.amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }\n.amenity {\n background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 600;\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-pill); padding: 5px 13px;\n}\n.cta {\n display: block; text-align: center; background: var(--orange); color: var(--white);\n font-weight: 600; font-family: var(--font-display); border-radius: var(--r-pill);\n padding: 15px; min-height: 48px;\n box-shadow: 0 8px 22px rgba(255, 106, 0, 0.34); transition: all 0.14s ease;\n}\n.cta:hover { background: var(--orange-deep); }\n.cta:active { transform: translateY(1px); }\n.panel .fine { font-size: 11.5px; color: var(--ink-3); margin-top: 14px; text-align: center; }\n\n/* ================= Lightbox ================= */\n.lightbox {\n position: fixed; inset: 0; background: rgba(20, 24, 20, 0.94); z-index: var(--z-modal, 900);\n display: flex; align-items: center; justify-content: center; cursor: zoom-out;\n padding: max(16px, env(safe-area-inset-top)) 16px;\n}\n.lightbox img { max-width: 94vw; max-height: 90vh; border-radius: 12px; }\n\n/* Lightbox v2 : balayage entre photos (scroll-snap) + pincement pour zoomer */\n.lightbox-v2 { padding: 0; cursor: default; display: block; }\n.lightbox-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%;\n -webkit-overflow-scrolling: touch; scrollbar-width: none;\n touch-action: pan-x pinch-zoom;\n}\n.lightbox-track::-webkit-scrollbar { display: none; }\n.lightbox-cell {\n flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center;\n display: flex; align-items: center; justify-content: center;\n padding: max(52px, env(safe-area-inset-top)) 10px max(24px, env(safe-area-inset-bottom));\n overflow: hidden;\n}\n.lightbox-cell img {\n max-width: 100%; max-height: 100%; border-radius: 10px;\n user-select: none; -webkit-user-drag: none; will-change: transform;\n}\n.lightbox-close {\n position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.4);\n background: rgba(20, 24, 20, 0.6); color: #fff; font-size: 18px; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.lightbox-count {\n position: fixed; left: 50%; transform: translateX(-50%); right: auto;\n top: max(22px, env(safe-area-inset-top)); bottom: auto; z-index: 3;\n}\n.lightbox-v2 .carousel-nav { position: fixed; z-index: 3; }\n\n/* visuel de secours (SmartImg) directement sous .carousel (galerie vide) */\n.carousel > img { display: block; width: 100%; aspect-ratio: 16/11; object-fit: cover; }\n\n/* ================= Sources page ================= */\n.sources { padding: 44px 0 90px; }\n.sources h1 { font-size: clamp(28px, 4vw, 40px); margin: 10px 0 6px; color: var(--navy); }\n.sources .sub { color: var(--ink-2); margin-bottom: 30px; max-width: 700px; }\n.src-wrap { overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); background: var(--surface); }\n.src-table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 720px; }\n.src-table th {\n text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: 0.1em; color: var(--ink-3); padding: 14px 18px;\n border-bottom: 1px solid var(--line-strong); background: var(--grey);\n}\n.src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }\n.src-table tr:last-child td { border-bottom: none; }\n.src-table tr:hover td { background: var(--navy-soft); }\n.src-table a { color: var(--blue); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.pill { display: inline-block; border-radius: var(--r-pill); padding: 3px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }\n.pill.ok { background: var(--blue-soft); color: var(--blue); }\n.pill.todo { background: var(--orange-soft); color: var(--orange-deep); }\n.count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--navy); }\n\n/* ================= Page Statistiques ================= */\n.stats-page { padding: 44px 0 90px; }\n.stats-title { font-size: clamp(30px, 4.6vw, 46px); margin: 10px 0 6px; color: var(--navy); }\n.stats-page .sub { color: var(--ink-2); max-width: 640px; margin-bottom: 30px; }\n\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 30px; }\n.tile {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n padding: 18px 20px; box-shadow: var(--shadow-flat);\n}\n.tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--navy); }\n.tile-k { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }\n.hero-tile { background: var(--navy); color: var(--white); border-color: var(--navy); }\n.hero-tile .tile-v { color: var(--white); }\n.hero-tile .tile-k { color: rgba(255, 255, 255, 0.6); }\n\n.viz-card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px 28px 20px; margin-bottom: 22px;\n}\n.viz-card h2 { font-size: 19px; letter-spacing: -0.01em; color: var(--navy); }\n.viz-sub { font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 4px 0 20px; }\n.viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n\n/* Histogramme (mono-série, bleu vif) */\n.histo { display: flex; align-items: stretch; gap: 2px; height: 240px; }\n.histo-col { flex: 1; display: flex; flex-direction: column; min-width: 0; cursor: default; }\n.histo-bar-zone { flex: 1; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line-strong); }\n.histo-bar {\n width: 100%; background: var(--blue); border-radius: 6px 6px 0 0;\n min-height: 2px; transition: background 0.12s ease;\n}\n.histo-col:hover .histo-bar { background: var(--orange); }\n.histo-x { font-size: 9.5px; color: var(--ink-3); text-align: left; height: 18px; padding-top: 5px; overflow: visible; white-space: nowrap; }\n\n/* Barres horizontales (mono-série) */\n.hbars { display: flex; flex-direction: column; gap: 7px; }\n.hbar-row {\n display: grid; grid-template-columns: minmax(96px, 170px) 1fr auto;\n gap: 12px; align-items: center; min-height: 26px; cursor: default;\n}\n.hbar-label { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.hbar-label a { border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.hbar-label a:hover { color: var(--blue); }\n.hbar-track { background: var(--grey); border-radius: var(--r-pill); height: 18px; overflow: hidden; }\n.hbar-fill {\n display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill);\n min-width: 2px; transition: background 0.12s ease;\n}\n.hbar-row:hover .hbar-fill { background: var(--orange); }\n.hbar-value { font-size: 11.5px; font-weight: 700; white-space: nowrap; color: var(--navy); }\n.hbar-value em { font-style: normal; font-weight: 500; color: var(--ink-3); }\n@media (max-width: 640px) {\n .hbar-row { grid-template-columns: minmax(80px, 110px) 1fr auto; gap: 8px; }\n .hbar-value em { display: none; }\n .viz-card { padding: 20px 16px 14px; }\n .histo { height: 170px; }\n}\n\n/* Infobulle */\n.viz-tip {\n position: fixed; z-index: var(--z-toast, 950); pointer-events: none; max-width: 180px;\n background: var(--navy); color: var(--white); border-radius: 12px;\n padding: 9px 12px; font-size: 12px; line-height: 1.5;\n box-shadow: 0 8px 24px rgba(20, 24, 20, 0.35);\n}\n.viz-tip-title { font-family: var(--font-display); font-weight: 700; color: var(--orange); margin-bottom: 2px; }\n\n/* Vue tableau repliable (accessibilité / relief) */\n.viz-table { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 10px; }\n.viz-table summary {\n cursor: pointer; font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em; color: var(--blue);\n}\n.viz-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }\n.viz-table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line-strong); }\n.viz-table td { padding: 6px 10px; border-bottom: 1px solid var(--line); }\n\n.stats-foot { font-size: 11.5px; color: var(--ink-3); letter-spacing: 0.02em; margin-top: 6px; }\n\n/* ================= Mobile : feuille de filtres + FAB ================= */\n.sheet-head { display: none; }\n.sheet-apply { display: none; }\n.sheet-backdrop { display: none; }\n.fab { display: none; }\n\n@media (max-width: 640px) {\n /* En-tête compact */\n .header-inner { height: 56px; }\n .brand { font-size: 21px; }\n .nav a { padding: 8px 13px; font-size: 13.5px; }\n .ticker { font-size: 10.5px; padding: 6px 0; }\n\n /* Héro resserré + stats en rangée défilante */\n .hero h1 { font-size: clamp(30px, 8.6vw, 42px); }\n .hero p.lede { font-size: 15px; }\n .stat-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 6px; margin-right: -16px; padding-right: 16px; }\n .stat-row::-webkit-scrollbar { display: none; }\n .stat-chip { flex: 0 0 auto; white-space: nowrap; }\n\n /* La barre de filtres devient une feuille coulissante (bottom sheet) */\n .filterbar { display: none; }\n .filterbar.open .f-primary { flex-direction: column; }\n .filterbar.open .f-search { align-self: stretch; max-width: none; min-height: 46px; }\n .filterbar.open .f-search input { font-size: 16px; } /* anti-zoom iOS */\n .filterbar.open .f-ctl select { max-width: none; width: 100%; }\n .filterbar.open .f-more { display: none; }\n .filterbar.open .f-adv { margin-top: 4px; }\n .filterbar.open {\n display: flex; flex-direction: column; gap: 12px;\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);\n margin: 0; border-radius: 24px 24px 0 0; border: none;\n max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;\n padding: 16px 18px calc(18px + env(safe-area-inset-bottom));\n box-shadow: 0 -16px 48px rgba(20, 24, 20, 0.28);\n animation: sheet-up 0.22s ease;\n }\n @keyframes sheet-up { from { transform: translateY(30%); opacity: 0.4; } to { transform: none; opacity: 1; } }\n .filterbar.open .sheet-head {\n display: flex; justify-content: space-between; align-items: center;\n font-family: var(--font-display); font-weight: 700; font-size: 17px;\n letter-spacing: -0.01em; color: var(--navy);\n position: sticky; top: -16px; background: var(--surface); padding: 6px 0 8px;\n border-bottom: 1px solid var(--line); margin-bottom: 2px; z-index: 1;\n }\n .sheet-close {\n border: 1.5px solid var(--ink); background: var(--surface); border-radius: 50%;\n width: 36px; height: 36px; font-size: 15px; cursor: pointer; line-height: 1; color: var(--navy);\n }\n .filterbar.open .sheet-apply { display: block; width: 100%; }\n .sheet-backdrop {\n display: block; position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.45); backdrop-filter: blur(2px);\n }\n\n /* Bouton flottant */\n .fab {\n display: flex; align-items: center; gap: 6px;\n position: fixed; left: 50%; transform: translateX(-50%);\n bottom: calc(74px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: 80;\n background: var(--orange); color: var(--white); border: none;\n border-radius: var(--r-pill); padding: 13px 24px; font-family: var(--font-display);\n font-weight: 600; font-size: 15px; cursor: pointer;\n box-shadow: 0 10px 28px rgba(255, 106, 0, 0.4);\n }\n .fab:active { transform: translateX(-50%) scale(0.97); }\n\n /* Fiche logement : vignettes en bande défilante + panneau non collant */\n .thumbs { display: flex; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }\n .thumbs::-webkit-scrollbar { display: none; }\n .thumbs button { flex: 0 0 96px; }\n .detail { padding-top: 20px; }\n .panel { padding: 20px; }\n .panel .price { font-size: 28px; }\n .results-head { margin-top: 20px; }\n .chips { margin-right: -16px; padding-right: 16px; }\n .notice { padding: 48px 16px; }\n}\n\n/* ================= Barre de navigation inférieure (mobile) ================= */\n.tabbar { display: none; }\n@media (max-width: 760px) {\n .tabbar {\n display: grid; grid-template-columns: repeat(4, 1fr);\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border-top: 1px solid var(--line);\n padding: 6px 4px calc(6px + env(safe-area-inset-bottom));\n }\n .tabbar a {\n display: flex; flex-direction: column; align-items: center; gap: 3px;\n padding: 5px 2px; min-height: 48px; justify-content: center;\n font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n border-radius: var(--r-ctl); transition: color 0.12s ease;\n }\n .tabbar a.active { color: var(--orange); }\n .tabbar a svg { display: block; }\n body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }\n .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }\n}\n\n/* Anti-zoom iOS : les champs doivent faire >= 16px\n (filet global : tokens.css force aussi 16 px au doigt) */\n@media (max-width: 900px) {\n .field input, .field select,\n .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }\n}\n\n/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */\n@media (pointer: coarse) {\n .brand { position: relative; }\n .brand::after { content: \"\"; position: absolute; inset: -8px; }\n}\n\n/* ================= Footer ================= */\n.footer { background: var(--navy); color: rgba(255, 255, 255, 0.72); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; }\n.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 32px; color: var(--white); letter-spacing: -0.03em; margin-bottom: 4px; display: flex; align-items: center; }\n.footer .fbrand .logo-ico { margin-right: 13px; }\n.footer .fbrand .ka { color: var(--orange); }\n.footer .fbaseline { color: rgba(255, 255, 255, 0.65); font-size: 14px; margin-bottom: 16px; }\n.footer .fbaseline .hl { color: var(--orange); font-weight: 600; }\n.footer b { color: var(--white); }\n.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }\n.footer .fmono { font-size: 11.5px; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.42); margin-top: 18px; }\n\n/* --- Carte interactive (mission 4) --------------------------------------- */\n.results-tools { display: flex; align-items: center; gap: 14px; }\n.view-toggle {\n display: inline-flex; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); overflow: hidden; background: var(--surface);\n box-shadow: var(--shadow-flat);\n}\n.view-toggle button {\n border: 0; background: transparent; padding: 7px 16px; cursor: pointer;\n font-size: 12.5px; font-weight: 600; color: var(--ink-2);\n}\n/* zone tactile >= 44 px au doigt (coarse pointer = mobile/tablette) */\n@media (pointer: coarse) {\n .view-toggle button { min-height: 44px; padding: 10px 18px; }\n}\n.view-toggle button + button { border-left: 1px solid var(--line); }\n.view-toggle button.on { background: var(--blue); color: var(--white); }\n\n.map-split {\n display: grid; grid-template-columns: minmax(300px, 400px) 1fr; gap: 18px;\n height: calc(100dvh - 170px); min-height: 420px;\n}\n.map-list {\n overflow-y: auto; display: flex; flex-direction: column; gap: 14px;\n padding-right: 4px; scrollbar-width: thin;\n}\n.map-list .card { margin: 0; flex: 0 0 auto; }\n.mapview {\n width: 100%; height: 100%; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.map-loading {\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-3); font-size: 13px;\n}\n\n/* mobile d'abord : carte plein écran, liste masquée (bascule via l'onglet) */\n@media (max-width: 780px) {\n .map-split { grid-template-columns: 1fr; height: calc(100dvh - 210px); }\n .map-list { display: none; }\n .results-tools { width: 100%; justify-content: space-between; }\n}\n\n/* popup MapLibre au style Lou-Ka */\n.maplibregl-popup-content {\n padding: 0; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); box-shadow: var(--shadow-off);\n font-family: var(--font-body);\n}\n.maplibregl-popup-close-button {\n font-size: 18px; padding: 2px 8px; color: var(--white); z-index: 2;\n text-shadow: 0 0 4px rgba(20, 24, 20, 0.8);\n}\n.mv-pop img, .mv-noimg { width: 100%; height: 130px; object-fit: cover; }\n.mv-noimg { display: flex; align-items: center; justify-content: center;\n font-size: 34px; background: var(--grey); }\n.mv-pop-body { padding: 10px 12px 12px; }\n.mv-pop-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--navy); }\n.mv-pop-price small { font-weight: 400; color: var(--ink-3); font-size: 12px; }\n.mv-pop-title { font-size: 13px; color: var(--ink-2); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mv-pop-meta { font-size: 11.5px; color: var(--ink-2); margin-top: 6px; }\n.mv-pop-src { font-size: 10px; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }\n.mv-pop-cta {\n display: block; margin-top: 10px; padding: 9px 10px; text-align: center;\n background: var(--orange); color: var(--white); border-radius: var(--r-pill);\n font-weight: 600; font-size: 13px;\n}\n.mv-pop-cta:hover { background: var(--orange-deep); }\n\n/* --- Commodités de proximité (fiche) -------------------------------------- */\n.poi-list {\n list-style: none; margin: 0; padding: 0;\n display: flex; flex-direction: column; gap: 2px;\n}\n.poi-list li {\n display: flex; align-items: center; gap: 9px;\n padding: 5px 2px; border-bottom: 1px solid var(--line);\n font-size: 13px;\n}\n.poi-list li:last-child { border-bottom: 0; }\n.poi-ico { width: 20px; text-align: center; flex: 0 0 auto; }\n.poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.poi-dist { font-size: 11.5px; color: var(--navy); font-weight: 600; flex: 0 0 auto; }\n\n/* --- Menu déroulant mobile ------------------------------------------------- */\n.menu-btn {\n display: none; position: relative; z-index: 60;\n width: 44px; height: 44px; margin-left: auto;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); cursor: pointer; padding: 0;\n flex-direction: column; align-items: center; justify-content: center; gap: 5px;\n box-shadow: var(--shadow-flat);\n transition: box-shadow 0.15s ease, transform 0.15s ease;\n}\n.menu-btn:active { transform: scale(0.96); }\n.menu-btn span {\n display: block; width: 18px; height: 2px; background: var(--navy);\n border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease;\n}\n.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }\n.menu-btn.open span:nth-child(2) { opacity: 0; }\n.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }\n\n.mobile-menu {\n display: none; position: absolute; left: 0; right: 0; top: 100%;\n background: var(--white); border-bottom: 1px solid var(--line);\n box-shadow: 0 18px 30px rgba(20, 24, 20, 0.14);\n padding: 8px 16px calc(16px + env(safe-area-inset-bottom));\n max-height: 0; overflow: hidden; opacity: 0;\n transition: max-height 0.3s ease, opacity 0.22s ease;\n}\n.mobile-menu { z-index: var(--z-modal, 900); }\n.mobile-menu.open { max-height: 480px; opacity: 1; }\n/* Le drawer vit DANS le header : quand il est ouvert, le header monte au niveau\n modal pour peindre au-dessus du .mm-backdrop (frère du header, overlay 800). */\n.header:has(.mobile-menu.open) { z-index: var(--z-modal, 900); }\n.mm-link {\n display: flex; align-items: center; gap: 12px;\n padding: 15px 10px; min-height: 52px;\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n letter-spacing: -0.02em; border-bottom: 1px solid var(--line);\n color: var(--navy);\n opacity: 0; transform: translateY(-8px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); }\n.mm-link.active { color: var(--orange); }\n.mm-link.active .mm-arrow { opacity: 1; color: var(--orange); }\n.mm-ico { width: 26px; text-align: center; font-size: 17px; }\n.mm-arrow { margin-left: auto; opacity: 0.25; color: var(--orange); transition: opacity 0.15s ease; }\n.mm-link:active { background: var(--orange-soft); border-radius: var(--r-ctl); }\n.mm-foot {\n padding: 12px 10px 4px;\n font-size: 11px; color: var(--ink-3); letter-spacing: 0.02em;\n}\n.mm-backdrop {\n position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px);\n}\n@media (max-width: 760px) {\n .menu-btn { display: flex; }\n .nav { display: none; }\n .mobile-menu { display: block; }\n}\n\n/* --- Bandeau de consentement (témoins) ------------------------------------- */\n.cookie-banner {\n position: fixed; left: 12px; right: 12px;\n bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80;\n animation: cookie-in 0.35s ease;\n}\n@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } }\n.cookie-inner {\n max-width: 720px; margin: 0 auto; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 16px 18px;\n}\n.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }\n.cookie-text b { color: var(--navy); }\n.cookie-text a { text-decoration: underline; text-underline-offset: 2px; }\n.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n.cookie-opt {\n display: flex; align-items: flex-start; gap: 10px; font-size: 13px;\n color: var(--ink-2); cursor: pointer;\n}\n.cookie-opt input { margin-top: 2px; accent-color: var(--orange); width: 16px; height: 16px; }\n.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; }\n@media (max-width: 640px) {\n .cookie-actions { justify-content: stretch; }\n .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; }\n}\n\n/* --- Page confidentialité + lien pied de page ------------------------------- */\n.legal { padding: 48px 0 70px; max-width: 760px; }\n.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; color: var(--navy); }\n.legal section { margin-top: 26px; }\n.legal h3 { font-size: 18px; margin-bottom: 8px; }\n.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }\n.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }\n.flink {\n border: 0; background: none; padding: 0; cursor: pointer;\n color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px;\n}\n\n/* --- Section « Le quartier » (fiche) --------------------------------------- */\n.quartier { margin-top: 34px; }\n.quartier h2 { font-size: 24px; letter-spacing: -0.02em; color: var(--navy); }\n.q-sub { color: var(--ink-3); font-size: 13px; margin: 4px 0 16px; }\n.q-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;\n}\n@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n.q-cell {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 12px 14px; box-shadow: var(--shadow-flat);\n}\n.q-val { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.q-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px; }\n.q-prox { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }\n.q-bar { display: flex; align-items: center; gap: 10px; font-size: 13px; }\n.q-bar-label { flex: 0 0 150px; color: var(--ink-2); }\n@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n.q-bar-track {\n flex: 1; height: 10px; background: var(--grey);\n border-radius: var(--r-pill); overflow: hidden;\n}\n.q-bar-fill { display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill); }\n.q-bar-num { flex: 0 0 30px; text-align: right;\n font-size: 11.5px; font-weight: 700; color: var(--navy); }\n.q-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.q-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 7px 14px; font-size: 12.5px; background: var(--surface);\n}\n.q-badge.cool { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.q-badge.hot { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.quartier .fine { margin-top: 10px; }\n\n/* --- Fiche v2 (mobile-first) ------------------------------------------------ */\n/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka\n « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`\n / `display:contents` pour réordonner — les blocs sans `order` retombaient à\n order:0 et passaient devant la galerie sur mobile. Mobile : colonnes\n empilées ; desktop : grid 2 colonnes (logement | synthèse). */\n.fiche { display: flex; flex-direction: column; gap: 22px; }\n.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n@media (min-width: 900px) {\n .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n .f-col { gap: 26px; }\n}\n.f-bloc { min-width: 0; }\n.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n.f-bloc:empty { display: none; }\n\n/* galerie à balayage natif */\n.carousel { position: relative; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); background: var(--grey); box-shadow: var(--shadow-off-soft); }\n.carousel-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch; scrollbar-width: none; aspect-ratio: 16/11;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track img {\n flex: 0 0 100%; width: 100%; object-fit: cover; scroll-snap-align: center;\n cursor: zoom-in;\n}\n.carousel-empty { display: flex; align-items: center; justify-content: center;\n aspect-ratio: 16/11; font-size: 48px; }\n.carousel-count {\n position: absolute; right: 12px; bottom: 12px; z-index: 2;\n background: rgba(20, 24, 20, 0.78); color: var(--white);\n font-size: 12px; font-weight: 600;\n padding: 4px 11px; border-radius: var(--r-pill);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.carousel-nav {\n position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--ink);\n background: rgba(255, 255, 255, 0.92); color: var(--navy); font-size: 22px; cursor: pointer;\n display: flex; align-items: center; justify-content: center; line-height: 1;\n box-shadow: var(--shadow-flat);\n}\n.carousel-nav.prev { left: 10px; } .carousel-nav.next { right: 10px; }\n@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n\n/* bandeau prix + badge marché */\n.f-hero .price { font-size: clamp(34px, 8vw, 44px); }\n.deal-badge {\n display: inline-flex; align-items: center; gap: 6px; margin: 8px 0 4px;\n border-radius: var(--r-pill); padding: 7px 14px; font-size: 13px; font-weight: 600;\n border: 1.5px solid var(--ink);\n}\n.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.deal-ok { background: var(--surface); color: var(--ink-2); }\n.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.f-hero h1 { font-size: clamp(20px, 4.5vw, 26px); margin-top: 8px; }\n\n/* chips clés à défilement horizontal */\n.chips-scroll {\n display: flex; gap: 8px; overflow-x: auto; padding: 12px 0 6px;\n scrollbar-width: none; -webkit-overflow-scrolling: touch;\n}\n.chips-scroll::-webkit-scrollbar { display: none; }\n.chip-key {\n flex: 0 0 auto; background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 8px 15px;\n font-size: 12px; font-weight: 600; white-space: nowrap; min-height: 34px;\n display: inline-flex; align-items: center;\n}\n\n/* ancres de navigation rapide */\n.ancres {\n display: flex; gap: 4px; overflow-x: auto; padding: 6px 0 2px;\n scrollbar-width: none; border-bottom: 1px solid var(--line); margin-bottom: 8px;\n}\n.ancres::-webkit-scrollbar { display: none; }\n.ancres a {\n flex: 0 0 auto; padding: 9px 14px; font-size: 13.5px; font-weight: 600;\n color: var(--ink-2); border-radius: var(--r-pill); min-height: 44px;\n display: inline-flex; align-items: center;\n}\n.ancres a:hover { background: var(--orange-soft); color: var(--orange); }\nhtml { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */\n\n/* description restructurée */\n.enbref {\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-card); padding: 12px 16px; color: var(--navy);\n font-size: 15px; margin: 0 0 14px;\n}\n.desc-section { margin-bottom: 12px; }\n.desc-section h4 { font-size: 15px; margin-bottom: 3px; }\n.desc-section p { color: var(--ink-2); font-size: 14.5px; margin: 0; white-space: pre-line; }\n.texte-original { margin-top: 12px; }\n.texte-original summary {\n cursor: pointer; font-size: 12.5px;\n color: var(--ink-3); min-height: 44px; display: flex; align-items: center;\n}\n.texte-original p { color: var(--ink-3); font-size: 13px; white-space: pre-line; }\n\n/* inclusions : confirmées vs mentionnées */\n.amenity.confirmed { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.25); color: var(--blue); font-weight: 600; }\n.amenity.unconfirmed { background: transparent; border-style: dashed; border-color: var(--line-strong); color: var(--ink-3); }\n\n/* détails pratiques : historique de prix */\n.prix-histo {\n margin-top: 12px; padding: 10px 14px; border-radius: var(--r-card);\n border: 1.5px solid var(--ink); font-size: 13.5px; background: var(--surface);\n}\n.prix-histo.down { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n\n/* quartier compacté sur mobile */\n@media (max-width: 640px) {\n .q-cell { padding: 9px 11px; }\n .q-val { font-size: 16px; }\n .q-bar { gap: 8px; font-size: 12px; }\n .q-bar-track { height: 8px; }\n}\n.q-badge-sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }\n\n/* POI groupés repliables */\n.poi-groupe { border: 1.5px solid var(--ink); border-radius: var(--r-card);\n margin-bottom: 8px; background: var(--surface); overflow: hidden; }\n.poi-groupe summary {\n display: flex; justify-content: space-between; align-items: center; gap: 10px;\n padding: 12px 14px; cursor: pointer; font-weight: 600; font-size: 14px;\n min-height: 48px; list-style: none;\n}\n.poi-groupe summary::-webkit-details-marker { display: none; }\n.poi-groupe summary::after { content: \"▾\"; color: var(--ink-3); transition: transform 0.15s ease; }\n.poi-groupe[open] summary::after { transform: rotate(180deg); }\n.poi-resume { font-size: 11.5px; color: var(--ink-3); font-weight: 400; }\n.poi-groupe .poi-list { padding: 0 14px 10px; }\n\n/* CTA sticky mobile */\n.cta-sticky {\n position: fixed; left: 0; right: 0; z-index: var(--z-dropdown, 700);\n bottom: var(--consent-h, 0px);\n display: flex; align-items: center; gap: 12px;\n background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(12px);\n border-top: 1px solid var(--line);\n padding: 10px 16px calc(10px + env(safe-area-inset-bottom));\n}\n.cta-sticky .cta { flex: 1; margin: 0; text-align: center; }\n.cta-sticky-prix { font-family: var(--font-display); font-weight: 700; font-size: 19px; white-space: nowrap; color: var(--navy); }\n.cta-sticky-prix small { font-weight: 500; font-size: 10px; color: var(--ink-3); }\n@media (min-width: 900px) { .cta-sticky { display: none; } }\n@media (max-width: 899px) {\n .cta-desktop { display: none; }\n .detail { padding-bottom: 84px; } /* place pour le CTA sticky */\n}\n.f-foot { margin-top: 26px; }\n\n/* --- Passerelle vers l'annonce originale ------------------------------------ */\n.passerelle {\n min-height: calc(100dvh - 120px); display: flex; align-items: center;\n justify-content: center; padding: 24px 16px;\n}\n.pass-card {\n width: 100%; max-width: 520px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 30px 28px; text-align: center;\n}\n.pass-brands {\n display: flex; align-items: center; justify-content: center; gap: 14px;\n margin-bottom: 22px; flex-wrap: wrap;\n}\n.pass-brand {\n font-family: var(--font-display); font-weight: 700; font-size: 24px;\n letter-spacing: -0.03em; display: inline-flex; align-items: center; color: var(--navy);\n}\n.pass-brand .ka { color: var(--orange); }\n.pass-trajet { display: inline-flex; align-items: center; gap: 5px; }\n.pass-dot {\n width: 6px; height: 6px; border-radius: 50%; background: var(--orange);\n opacity: 0.25; animation: pass-dot 1.2s ease infinite;\n}\n.pass-dot:nth-child(2) { animation-delay: 0.2s; }\n.pass-dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes pass-dot { 40% { opacity: 1; transform: translateX(2px); } }\n.pass-fleche { color: var(--orange); font-size: 18px; }\n.pass-gestionnaire {\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n display: inline-flex; align-items: center; gap: 8px;\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-pill); padding: 6px 16px; color: var(--blue);\n}\n.pass-card h1 { font-size: 20px; letter-spacing: -0.02em; }\n.pass-texte { color: var(--ink-2); font-size: 14.5px; margin: 10px 0 20px; }\n.pass-progress {\n height: 10px; border-radius: var(--r-pill);\n overflow: hidden; background: var(--grey); margin-bottom: 20px;\n}\n.pass-progress-fill {\n display: block; height: 100%; width: 0; background: var(--orange);\n border-radius: var(--r-pill);\n}\n.pass-progress-fill.go { animation: pass-fill 2.2s linear forwards; }\n@keyframes pass-fill { to { width: 100%; } }\n@media (prefers-reduced-motion: reduce) {\n .pass-progress-fill.go { animation: none; width: 100%; }\n .pass-dot { animation: none; opacity: 0.7; }\n}\n.pass-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; }\n\n/* --- boutons PDF -------------------------------------------------------- */\n.btn-pdf { display: block; text-align: center; margin-top: 10px; width: 100%; }\n\n/* --- Onglets de la page Stats -------------------------------------------- */\n.onglets {\n display: flex; gap: 8px; overflow-x: auto; margin: 26px 0 22px;\n padding-bottom: 4px; scrollbar-width: none;\n border-bottom: 1px solid var(--line);\n}\n.onglets::-webkit-scrollbar { display: none; }\n.onglet {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;\n border: 1.5px solid var(--ink);\n border-radius: var(--r-pill);\n background: var(--surface); color: var(--ink-2); cursor: pointer;\n padding: 10px 18px; font-weight: 600; font-size: 13.5px; min-height: 44px;\n margin-bottom: 8px;\n transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;\n}\n.onglet:hover { background: var(--blue-soft); color: var(--blue); border-color: var(--blue); }\n.onglet.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* baisses de prix */\n.baisses { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }\n.baisses li {\n display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n padding: 9px 4px; border-bottom: 1px solid var(--line); font-size: 13.5px;\n}\n.baisses li a { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }\n.baisse-ville { color: var(--ink-3); font-size: 11.5px; }\n.baisse-prix { margin-left: auto; }\n.baisse-prix s { color: var(--ink-3); }\n.baisse-pct {\n font-style: normal; font-weight: 700;\n background: var(--blue-soft);\n color: var(--blue); border-radius: var(--r-pill); padding: 2px 9px;\n font-size: 11px; margin-left: 8px;\n}\n.alertes { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n\n/* ================= Pagination (12 annonces/page) ================= */\n.pager {\n display: flex; align-items: center; justify-content: space-between;\n gap: 16px; flex-wrap: wrap; margin: 26px 0 10px; padding-top: 18px;\n border-top: 1px solid var(--line);\n}\n.pager-info {\n font-size: 12.5px; color: var(--ink-2); letter-spacing: 0.02em;\n}\n.pager-info em { font-style: normal; color: var(--ink-3); }\n.pager-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.pager-btn {\n min-width: 38px; height: 38px; padding: 0 10px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); cursor: pointer;\n font-size: 13px; font-weight: 600;\n color: var(--navy); transition: all 0.12s ease;\n}\n.pager-btn:hover:not(:disabled):not(.on) {\n background: var(--blue-soft); border-color: var(--blue); color: var(--blue);\n}\n.pager-btn.on { background: var(--navy); border-color: var(--navy); color: var(--white); cursor: default; }\n.pager-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n.pager-ellipsis { color: var(--ink-3); padding: 0 2px; }\n.map-list .pager { margin: 10px 0 4px; }\n@media (max-width: 640px) {\n .pager { justify-content: center; }\n .pager-info { width: 100%; text-align: center; order: 2; }\n}\n\n/* ================= Icônes SVG inline — alignement ================= */\n.chip-ico { display: inline-flex; align-items: center; gap: 6px; }\n.chip-ico svg { flex: none; }\n.view-toggle button, .seg button { display: inline-flex; align-items: center; gap: 6px; }\n.fab { align-items: center; gap: 8px; }\n.fab svg { flex: none; }\n.mm-ico { display: inline-flex; align-items: center; }\n.notice .big { display: flex; justify-content: center; color: var(--ink-3); }\n.notice .big svg { display: block; }\n.card-img .noimg { color: var(--ink-3); }\n.badge.right { display: inline-flex; align-items: center; gap: 4px; }\n.btn-pdf { display: inline-flex; align-items: center; gap: 7px; }\n.f-search svg { flex: none; color: var(--ink-3); }\n.mv-noimg { display: flex; align-items: center; justify-content: center; color: var(--ink-3); }\n\n/* footer — liens (Groupe KA, courriels, mentions légales) */\n.footer .frow a { color: var(--white); text-decoration: underline; text-decoration-color: rgba(255, 106, 0, 0.5); text-underline-offset: 3px; }\n.footer .frow a:hover { color: var(--orange); }\n.footer .fmono a:hover { color: var(--orange); }\n\n/* ================= Compte (connexion Google) ================= */\n.login-btn {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 8px 16px; margin-left: 10px; min-height: 38px;\n background: var(--navy); border: none;\n border-radius: var(--r-pill); font-weight: 600; font-size: 13.5px; color: var(--white);\n transition: all 0.12s ease; white-space: nowrap;\n box-shadow: 0 4px 14px rgba(20, 24, 20, 0.22);\n}\n.login-btn:hover { background: var(--blue); }\n.account { position: relative; margin-left: 10px; }\n.account-btn {\n width: 38px; height: 38px; padding: 0; border: 2px solid var(--orange);\n border-radius: var(--r-pill); overflow: hidden; cursor: pointer;\n background: var(--orange-soft); display: flex; align-items: center; justify-content: center;\n}\n.account-btn img { width: 100%; height: 100%; object-fit: cover; }\n.account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }\n/* Click-catcher plein écran rendu via createPortal à la racine de \n (un fixed descendant du header blurré se re-parentait → 66 px de haut).\n z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le\n header et doit peindre au-dessus du voile. */\n.account-backdrop { position: fixed; inset: 0; z-index: 40; }\n.account-menu {\n position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;\n min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off); overflow: hidden;\n}\n.account-id { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-bottom: 1px solid var(--line); }\n.account-id b { font-size: 14px; }\n.account-id span { font-size: 11.5px; color: var(--ink-3); word-break: break-all; }\n.account-menu button {\n width: 100%; text-align: left; padding: 11px 14px; border: 0; cursor: pointer;\n background: transparent; font-weight: 600; font-size: 13.5px; color: var(--danger);\n}\n.account-menu button:hover { background: var(--orange-soft); color: var(--navy); }\n@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n\n/* ================= Profil — carte de membre Groupe KA ================= */\n.profil { padding-top: 40px; padding-bottom: 60px; }\n.profil h1 { font-size: clamp(30px, 5vw, 44px); margin: 10px 0 26px; color: var(--navy); }\n.profil .lede { max-width: 520px; color: var(--ink-2); margin-bottom: 22px; }\n\n.pc {\n position: relative; max-width: 520px; overflow: hidden;\n background: linear-gradient(135deg, #101a40 0%, var(--navy) 55%, #080e24 100%);\n color: var(--white);\n border-radius: 20px;\n padding: 24px 26px 0; box-shadow: var(--shadow-off);\n}\n.pc-watermark {\n position: absolute; right: -18px; top: -34px; pointer-events: none;\n font-family: var(--font-display); font-weight: 700; font-size: 170px;\n letter-spacing: -0.06em; color: rgba(255, 106, 0, 0.07);\n transform: rotate(-8deg); line-height: 1;\n}\n.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }\n.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }\n.pc-ka { color: var(--orange); margin-left: 1px; display: inline-block; }\n.pc-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.5); }\n.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }\n.pc-id-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2em; color: rgba(255, 106, 0, 0.75); }\n.pc-id {\n font-weight: 700; color: var(--orange);\n font-size: clamp(22px, 6vw, 34px); letter-spacing: 0.14em;\n text-shadow: 0 0 24px rgba(255, 106, 0, 0.35);\n}\n.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }\n.pc-holder { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pc-holder-name { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pc-holder-since { font-size: 10.5px; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.06em; }\n.pc-role-badge {\n display: inline-block; width: fit-content; margin: 3px 0 2px;\n background: var(--orange); color: var(--white);\n font-size: 9px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.12em;\n padding: 2px 8px 3px; border-radius: var(--r-pill);\n}\n.pc-avatar { width: 52px; height: 52px; border-radius: var(--r-pill); border: 2px solid var(--orange); flex: none; }\n.pc-strip { display: flex; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(255, 106, 0, 0.1); border-top: 1px solid rgba(255, 106, 0, 0.25); overflow: hidden; }\n.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--orange); opacity: 0.7; }\n.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.35; }\n.pc-strip i:nth-child(3n+1) { height: 9px; }\n.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.9; }\n.pc-copy { margin-top: 16px; }\n.pc-copy.ok { background: var(--blue); color: var(--white); }\n\n.profil-grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 12px; margin-top: 30px; max-width: 760px;\n}\n.pg-item {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 14px 16px;\n display: flex; flex-direction: column; gap: 4px; box-shadow: var(--shadow-flat);\n}\n.pg-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }\n.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }\n.pg-value.mono { color: var(--blue); }\n\n.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }\n.profil-note a { text-decoration: underline; text-underline-offset: 3px; }\n.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }\n.profil-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n\n.account-kaid { font-size: 10.5px; color: var(--blue); letter-spacing: 0.08em; }\n.account-link {\n display: block; width: 100%; text-align: left; padding: 11px 14px;\n font-weight: 600; font-size: 13.5px; color: var(--navy);\n border-bottom: 1px solid var(--line);\n}\n.account-link:hover { background: var(--orange-soft); }\n\n/* profil public — section + lien partageable */\n.profil-public { margin-top: 30px; max-width: 640px; }\n.profil-public h3 { font-size: 18px; margin-bottom: 6px; }\n.profil-public p { color: var(--ink-2); font-size: 13.5px; margin: 0 0 12px; }\n.public-link {\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n margin-top: 14px; padding: 12px 14px; background: var(--blue-soft);\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-card);\n}\n.public-link a { font-size: 13px; color: var(--blue); text-decoration: underline; text-underline-offset: 3px; word-break: break-all; }\n\n/* ================= Bienvenue — choix du rôle ================= */\n.bienvenue { padding-top: 44px; padding-bottom: 70px; }\n.bienvenue h1 { font-size: clamp(30px, 5vw, 46px); margin: 10px 0 14px; color: var(--navy); }\n.bienvenue .lede { max-width: 480px; color: var(--ink-2); margin-bottom: 30px; }\n.role-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; max-width: 780px; }\n.role-card {\n display: flex; flex-direction: column; align-items: flex-start; gap: 10px;\n text-align: left; padding: 24px 24px 20px; cursor: pointer;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-flat);\n font-family: var(--font-body); transition: all 0.15s ease;\n}\n.role-card:hover:not(:disabled) { transform: translateY(-4px); box-shadow: var(--shadow-off); border-color: rgba(255, 106, 0, 0.4); }\n.role-card:disabled { opacity: 0.6; cursor: wait; }\n.role-ico { display: flex; width: 54px; height: 54px; align-items: center; justify-content: center; background: var(--orange-soft); color: var(--orange); border-radius: 16px; }\n.role-title { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.role-desc { color: var(--ink-2); font-size: 13.5px; }\n.role-perks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }\n.role-perks li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }\n.role-perks li::before {\n content: \"✓\"; flex: none; width: 16px; height: 16px; border-radius: 50%;\n background: var(--orange); color: var(--white); font-size: 10px; font-weight: 700;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.role-cta { margin-top: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--orange); }\n\n/* ================= Favoris — cœur sur les cartes ================= */\n.fav-btn {\n position: absolute; right: 10px; bottom: 10px; z-index: 2;\n width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;\n background: rgba(255, 255, 255, 0.94); border: none;\n border-radius: var(--r-pill); cursor: pointer; color: var(--navy);\n box-shadow: var(--shadow-flat);\n transition: transform 0.12s ease, background 0.12s ease, color 0.12s ease;\n}\n.fav-btn:hover { transform: scale(1.12); }\n.fav-btn.on { background: var(--white); color: var(--orange); }\n.fav-btn.on svg { filter: drop-shadow(0 0 3px rgba(255, 106, 0, 0.35)); }\n\n/* ================= Espace gestionnaire ================= */\n.claim-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; max-width: 640px; }\n.claim-row select {\n flex: 1; min-width: 260px; padding: 11px 14px; font-size: 14px;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); font-family: var(--font-body);\n}\n.claim-error { color: var(--danger); font-size: 13.5px; font-weight: 600; margin-top: 10px; }\n.org-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 22px; }\n.org-stat { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); }\n.org-form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }\n.org-form h3 { font-size: 18px; }\n.org-form label { display: flex; flex-direction: column; gap: 5px; flex: 1; }\n.org-form label span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }\n.org-form input, .org-form textarea {\n padding: 10px 14px; font-size: 14px; font-family: var(--font-body);\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);\n}\n.org-form textarea { resize: vertical; }\n.org-form input:focus, .org-form textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.org-form-row { display: flex; gap: 12px; flex-wrap: wrap; }\n.org-form .btn { align-self: flex-start; }\n\n/* ================= Page publique /g/{source} ================= */\n.orgpub-head { display: flex; gap: 20px; align-items: flex-start; margin-top: 8px; }\n.orgpub-logo { width: 84px; height: 84px; object-fit: contain; background: var(--surface); border: 1.5px solid var(--ink); border-radius: 18px; padding: 8px; flex: none; box-shadow: var(--shadow-flat); }\n.orgpub-id h1 { font-size: clamp(26px, 4.4vw, 40px); margin: 6px 0 8px; color: var(--navy); }\n.orgpub-tagline { font-size: 16px; color: var(--ink-2); margin: 0 0 12px; max-width: 640px; }\n.orgpub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.orgpub-meta a.stat-chip:hover { background: var(--blue-soft); }\n.orgpub-verified { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); font-weight: 600; }\n.orgpub-desc { max-width: 720px; color: var(--ink-2); margin: 18px 0 6px; white-space: pre-line; }\n.orgpub .results-head h2 { display: inline-flex; align-items: center; gap: 8px; }\n@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n\n/* ================= Éditeur de profil (photo, bio, réseaux) ================= */\n.profil-edit { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--line-strong); }\n/* profil géré au hub Groupe KA — lecture seule */\n.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }\n.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }\n.hub-profile a.stat-chip:hover { background: var(--blue-soft); }\n.hub-profile .btn-primary { display: inline-flex; align-items: center; gap: 7px; }\n.pe-avatar-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.pe-avatar {\n width: 84px; height: 84px; border-radius: var(--r-pill); object-fit: cover;\n border: 2.5px solid var(--orange); box-shadow: var(--shadow-flat);\n display: flex; align-items: center; justify-content: center;\n}\n.pe-avatar-empty { background: var(--orange-soft); color: var(--orange); font-family: var(--font-display); font-weight: 700; font-size: 34px; }\n.pe-avatar-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }\n.pe-avatar-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n.pe-hint { font-size: 11px; color: var(--ink-3); }\n.pe-socials-title { font-size: 15px; margin-top: 6px; }\n.pe-socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }\n.pe-social { display: flex !important; flex-direction: row !important; align-items: center; gap: 0 !important; }\n.pe-social-ico {\n display: flex; align-items: center; justify-content: center;\n width: 40px; align-self: stretch; flex: none;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-ctl) 0 0 var(--r-ctl);\n}\n.pe-social input { flex: 1; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; border-left: 0; }\n\n/* profil public — à propos + réseaux */\n.pub-about { max-width: 640px; margin-top: 22px; }\n.pub-bio { font-size: 15.5px; color: var(--ink); margin: 0 0 12px; white-space: pre-line; }\n.pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.pub-socials { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }\n.pub-social {\n display: flex; align-items: center; justify-content: center;\n width: 44px; height: 44px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: 14px; color: var(--navy);\n transition: all 0.12s ease;\n}\n.pub-social:hover { background: var(--navy); border-color: var(--navy); color: var(--white); transform: translateY(-2px); box-shadow: var(--shadow-off-soft); }\n\n/* -----------------------------------------------------------------------------\n Lou-Ka Maps (framework Ka Maps par Groupe Ka)\n Jetons du design system appliqués au chrome de la carte + sync liste↔carte.\n----------------------------------------------------------------------------- */\n.mapview .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n}\n.mapview { position: relative; overflow: hidden; }\n.mapview .ka-map { position: absolute; inset: 0; }\n\n/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en orange */\n.lmap3d {\n position: relative; height: 340px; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.lmap3d .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n position: absolute; inset: 0;\n}\n.lmap3d-skel {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.lmap3d-legende {\n position: absolute; left: 10px; bottom: 10px; z-index: 5;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 4px 10px; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n font-size: 10.5px; font-weight: 500; color: var(--ink-2);\n pointer-events: none;\n}\n.lmap3d-legende i {\n width: 10px; height: 10px; border-radius: 3px;\n background: var(--orange); border: 1px solid rgba(20, 24, 20, 0.25);\n}\n@media (max-width: 780px) { .lmap3d { height: 280px; } }\n.ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }\n.ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }\n.ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }\n\n/* carte de liste synchronisée avec la carte (survol / sélection) */\n.map-card { border-radius: var(--r-card); }\n.map-card-sel { outline: 2px solid var(--blue); outline-offset: 2px; }\n\n/* =============================================================================\n HARMONISATION « ÉDITORIAL SHARP » (Groupe KA) — couche finale posée après\n les styles historiques : géométrie 10/6 px, bordures encre 1,5 px, ombres\n décalées dures, accent orange Lou-Ka. La carte Ka Maps (Mapbox) conserve\n son propre thème marine/orange à l'intérieur du canevas.\n============================================================================= */\n\n/* — contrôles : plus de pilules (les chips/badges gardent --r-pill) — */\n.nav a, .login-btn, .cta, .mv-pop-cta, .f-search, .f-more, .pager-btn,\n.onglet, .view-toggle, .fab, .f-ctl, .pass-gestionnaire, .hbar-track,\n.q-bar-track, .pass-progress { border-radius: var(--r-ctl); }\n.seg button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); }\n.seg button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }\n\n/* — header : papier translucide + trait d'encre — */\n.header {\n background: rgba(245, 243, 238, 0.92);\n border-bottom: 1.5px solid var(--ink);\n}\n.nav a { font-family: var(--font-display); }\n.nav a:hover { color: var(--accent-deep); background: var(--accent-soft); }\n.nav a.active { background: var(--ink); color: var(--accent); }\n.brand .ka {\n background: var(--accent); color: var(--on-accent);\n border-radius: 6px; padding: 0 6px 2px; margin-left: 2px; transform: rotate(-2deg);\n}\n.header .gk-badge { margin-left: 4px; flex: none; }\n@media (max-width: 1080px) { .header .gk-badge { display: none; } }\n.mm-badge {\n display: flex; align-items: center; min-height: var(--touch);\n padding: 8px 10px 2px;\n}\n\n/* — ticker : encre + mono — */\n.ticker { font-family: var(--font-mono); border-bottom: 1.5px solid var(--ink); }\n\n/* — états sélectionnés : encre + accent (fini le bleu) — */\n.seg button.on, .onglet.on, .view-toggle button.on, .f-more.on {\n background: var(--ink); border-color: var(--ink); color: var(--accent);\n}\n.pager-btn.on { background: var(--ink); border-color: var(--ink); color: var(--accent); }\n\n/* — CTA / FAB : aplat accent, bordure encre, ombre décalée — */\n.cta, .mv-pop-cta, .fab {\n background: var(--accent); color: var(--on-accent);\n border: 1.5px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);\n font-family: var(--font-display); font-weight: 700;\n}\n.cta:hover, .mv-pop-cta:hover, .fab:hover { background: var(--accent-deep); }\n.cta:active, .fab:active { box-shadow: none; }\n.fab { box-shadow: 4px 4px 0 var(--ink), 0 10px 28px rgba(20, 24, 20, 0.25); }\n.login-btn {\n background: var(--ink); color: var(--accent);\n border: 1.5px solid var(--ink); box-shadow: none;\n font-family: var(--font-display); font-weight: 700;\n}\n.login-btn:hover { background: var(--accent-deep); color: var(--on-accent); }\n\n/* — cartes : levée nette diagonale (signature du groupe) — */\n.card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }\n.role-card:hover:not(:disabled) { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); border-color: var(--ink); }\n.badge { border: 1.5px solid var(--ink); border-radius: var(--r-ctl); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.badge.type { background: var(--accent); color: var(--on-accent); }\n.source-tag { font-family: var(--font-mono); }\n\n/* — barre d'onglets mobile : papier + encre, actif = accent orange — */\n@media (max-width: 760px) {\n .tabbar { background: rgba(245, 243, 238, 0.95); border-top: 1.5px solid var(--ink); }\n .tabbar a { font-family: var(--font-display); }\n .tabbar a.active { color: var(--accent); }\n}\n.mobile-menu { background: var(--paper); border-bottom: 1.5px solid var(--ink); }\n\n/* — carte de membre KA : encre plutôt que marine — */\n.pc { background: linear-gradient(135deg, #1d231d 0%, var(--ink) 55%, #0b0e0b 100%); }\n\n/* — micro-étiquettes en mono (système) — */\n.f-ctl > span, .f-group > label, .field label, .tile-k, .pg-label,\n.pc-label, .pc-id-label, .crumbs, .viz-sub { font-family: var(--font-mono); }\n\n/* — page Contact (design system) — */\n.contact-page { padding: 44px 0 90px; }\n.contact-page h1 { margin: 10px 0 8px; }\n.contact-page .lede { color: var(--ink-2); max-width: 640px; margin: 0 0 26px; }\n.contact-grid { display: grid; gap: 16px; grid-template-columns: 1fr; margin: 26px 0; }\n@media (min-width: 768px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }\n.contact-card { padding: 18px 20px; }\n.contact-card .klabel { display: block; margin-bottom: 6px; }\n.contact-card a.mail {\n font-family: var(--font-mono); font-weight: 700; font-size: 13.5px;\n color: var(--accent-deep); word-break: break-all;\n text-decoration: underline; text-underline-offset: 3px;\n display: inline-flex; align-items: center; min-height: 44px;\n}\n.contact-card p { margin: 6px 0 0; color: var(--ink-2); font-size: 13.5px; }\n.contact-note {\n border-left: 3px solid var(--accent); background: var(--surface);\n border-top: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);\n border-bottom: 1.5px solid var(--ink); border-radius: 0 var(--r-card) var(--r-card) 0;\n padding: 14px 18px; max-width: 720px; color: var(--ink-2); font-size: 14px;\n}\n.contact-note b { color: var(--ink); }\n.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.contact-sites a {\n display: inline-flex; align-items: center; min-height: 34px;\n padding: 4px 12px; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); font-family: var(--font-mono); font-size: 11px;\n font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;\n transition: transform 0.12s, box-shadow 0.12s;\n}\n.contact-sites a:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.contact-hub { margin-top: 26px; }\n\n/* — rangée de navigation locale au-dessus du footer commun — */\n.prefooter { border-top: 1.5px solid var(--ink); background: var(--surface-2); padding: 18px 0; }\n.prefooter .container {\n display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px;\n font-family: var(--font-mono); font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n}\n.prefooter a, .prefooter .flink {\n color: var(--ink-2); text-decoration: none; text-transform: uppercase;\n letter-spacing: 0.08em; font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;\n min-height: var(--touch); display: inline-flex; align-items: center;\n}\n.prefooter a:hover, .prefooter .flink:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }\n.prefooter .baseline { width: 100%; font-family: var(--font-body); font-weight: 400; font-size: 13px; color: var(--ink-2); text-transform: none; letter-spacing: 0; }\n.ka-footer { margin-top: 0; }\n@media (max-width: 760px) { .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } }\n\n/* ============ Juste valeur (fair value) — pastilles & analyse de prix ====== */\n.fv-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border-radius: var(--r-pill); padding: 6px 12px;\n font-size: 12.5px; font-weight: 700; border: 1.5px solid var(--ink);\n white-space: nowrap;\n}\n.fv-badge .fv-ico { font-size: 10px; line-height: 1; }\n.fv-badge .fv-pct { margin-left: 4px; }\n.fv-badge.fv-compact { padding: 2px 8px; font-size: 11px; gap: 4px; margin-left: 8px; vertical-align: 2px; }\n/* états (mêmes jetons que les badges marché existants — daltonien-compatible :\n l'icône ▼ ≈ ▲ et le libellé portent l'information, pas la couleur seule) */\n.fv-badge.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.35); color: var(--blue); }\n.fv-badge.deal-ok { background: var(--surface); color: var(--ink-2); }\n.fv-badge.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.4); color: var(--orange-deep); }\n\n.f-fairvalue .fv-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 12px; flex-wrap: wrap; margin-bottom: 6px;\n}\n.f-fairvalue .fv-value { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--navy); }\n.f-fairvalue .fv-value small { font-size: 12px; font-weight: 500; color: var(--ink-3); }\n.f-fairvalue .fv-range { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.fv-histo { display: block; width: 100%; height: auto; margin: 10px 0 4px; }\n.fv-histo-lbl { font: 700 10px Inter, system-ui, sans-serif; fill: var(--ink); }\n.fv-histo-lbl-fv { fill: var(--accent-deep, #cc5500); }\n.fv-histo-axis { font: 500 9px Inter, system-ui, sans-serif; fill: var(--ink-3); }\n.fv-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12.5px; color: var(--ink-2); margin: 6px 0 4px; }\n.fv-demo { display: flex; flex-wrap: wrap; gap: 8px; }\n.chip.chip-deal.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* =============================================================================\n MapSearch — recherche synchronisée liste ↔ carte (search/MapSearch.tsx)\n Une seule vérité partagée : compteur, tri, survols et sélections miroirs.\n============================================================================= */\n\n.ms { position: relative; }\n.ms-list { position: relative; scroll-behavior: smooth; }\n\n.ms-list-head {\n display: flex; align-items: center; justify-content: space-between;\n gap: 10px; flex-wrap: wrap; padding: 2px 2px 4px;\n}\n.ms-count { font-size: 13.5px; color: var(--ink-2); }\n.ms-count b { color: var(--ink); font-size: 15px; }\n.ms-nopos {\n margin-left: 8px; font-size: 11px; color: var(--ink-3);\n border: 1px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 8px; cursor: help;\n}\n.ms-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }\n.ms-sort select {\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); padding: 6px 8px; font-size: 12.5px;\n font-weight: 600; color: var(--ink);\n}\n\n.ms-pills { margin: 0 0 4px; }\n\n.ms-refit {\n align-self: flex-start; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--ink-2);\n cursor: pointer; box-shadow: var(--shadow-off-soft);\n}\n.ms-refit:hover { background: var(--navy); color: var(--white); border-color: var(--navy); }\n\n/* barre de progression fine — jamais d'écran qui clignote */\n.ms-progress {\n position: sticky; top: 0; z-index: 4; height: 3px; border-radius: 2px;\n background: linear-gradient(90deg, transparent, var(--accent), transparent);\n background-size: 200% 100%; animation: ms-progress 1s linear infinite;\n}\n@keyframes ms-progress { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n.ms-empty {\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-card);\n padding: 26px 18px; text-align: center; color: var(--ink-2);\n}\n.ms-empty .big { margin-bottom: 6px; color: var(--ink-3); }\n.ms-empty h3 { margin: 0 0 6px; font-size: 16px; color: var(--ink); }\n.ms-empty p { margin: 0 0 14px; font-size: 13px; }\n.ms-empty-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n/* miroirs survol / sélection venus de la carte */\n.map-card { transition: transform 0.15s ease, box-shadow 0.15s ease; }\n.map-card-hover {\n outline: 2px solid var(--accent); outline-offset: 2px;\n transform: translateY(-1px);\n}\n.map-card-pulse { animation: ms-pulse 0.9s ease 1; }\n@keyframes ms-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* indicateur discret : l'annonce survolée sur la carte est hors écran */\n.ms-hover-hint {\n position: sticky; z-index: 5; align-self: center;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 5px 12px;\n font-size: 11.5px; font-weight: 600; pointer-events: none;\n box-shadow: var(--shadow-off-soft);\n}\n.ms-hover-hint.haut { top: 8px; order: -1; }\n.ms-hover-hint.bas { bottom: 8px; }\n\n/* ---------------- carrousel mobile synchronisé aux marqueurs ------------- */\n.ms-carousel {\n position: absolute; left: 0; right: 0; bottom: 10px; z-index: 12;\n display: flex; gap: 10px; overflow-x: auto; padding: 4px 12px;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n.ms-carousel::-webkit-scrollbar { display: none; }\n.ms-car-item {\n flex: 0 0 78%; max-width: 340px; scroll-snap-align: center;\n display: grid; grid-template-columns: 108px 1fr; gap: 0;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); overflow: hidden;\n box-shadow: var(--shadow-off-soft); text-decoration: none; color: inherit;\n}\n.ms-car-item.sel { outline: 2.5px solid var(--accent); outline-offset: 1px; }\n.ms-car-item img, .ms-car-item .smart-img, .ms-car-item .card-fallback {\n width: 108px; height: 96px; object-fit: cover; display: block;\n}\n.ms-car-body { padding: 9px 12px; min-width: 0; }\n.ms-car-price { font-size: 14.5px; font-weight: 700; color: var(--ink); display: flex; gap: 6px; align-items: center; }\n.ms-car-title {\n font-size: 12px; color: var(--ink-2); margin-top: 2px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.ms-car-meta { font-size: 11px; color: var(--ink-3); margin-top: 3px; }\n.ms-car-more {\n flex: 0 0 auto; scroll-snap-align: center; align-self: stretch;\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n background: var(--navy); color: var(--white);\n font-size: 12.5px; font-weight: 600; padding: 0 18px; cursor: pointer;\n}\n\n/* mobile : carte plein cadre, liste masquée, carrousel par-dessus la carte */\n@media (max-width: 780px) {\n .ms { height: calc(100dvh - 190px); }\n .ms-list { display: none; }\n .ms .mapview { height: 100%; }\n .ka-search-area { top: 8px; }\n}\n@media (min-width: 781px) {\n .ms-carousel { display: none; }\n}\n\n/* mobile : réorganisation du chrome carte autour du carrousel */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 56px; left: 10px; bottom: auto; }\n .ms .ka-loading { bottom: 132px; }\n .ms .ka-empty { transform: translate(-50%, -70%); }\n}\n/* l'auto-défilement vers la carte laisse l'entête de résultats visible */\n.ms { scroll-margin-top: 108px; }\n\n/* mobile, vue carte : chrome sans chevauchement (case en haut, outils sous\n elle, FAB Filtres au-dessus du carrousel) */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 88px; left: 10px; bottom: auto; }\n .ms .ka-locate { top: 88px; right: 10px; }\n .fab-carte { bottom: 148px; }\n}\n\n/* =============================================================================\n KA Scores — pastilles, jauges circulaires, page méthodologie\n============================================================================= */\n.ka-badge {\n display: inline-flex; align-items: center; gap: 4px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 1px 8px 1px 3px; font-size: 11px; font-weight: 700;\n color: var(--ink); background: var(--surface); line-height: 1.5;\n vertical-align: middle;\n}\n.ka-badge-logo {\n background: var(--ink); color: var(--white); border-radius: 999px;\n font-size: 8.5px; font-weight: 800; padding: 1.5px 5px; letter-spacing: 0.4px;\n}\n.ka-badge.ka-haut { border-color: var(--green, #256d43); }\n.ka-badge.ka-haut .ka-badge-logo { background: var(--green, #256d43); }\n.ka-badge.ka-bon { border-color: var(--accent); }\n.ka-badge.ka-bon .ka-badge-logo { background: var(--accent); }\n.ka-badge.ka-moyen { opacity: 0.9; }\n.ka-badge.ka-bas { opacity: 0.75; }\n\n.ka-circles, .ka-demo-circles {\n display: flex; gap: 18px; flex-wrap: wrap; margin: 14px 0 6px;\n}\n.ka-circle { text-align: center; width: 92px; }\n.ka-circle-fond {\n fill: none; stroke: var(--line); stroke-width: 6;\n}\n.ka-circle-arc {\n fill: none; stroke: var(--ink-3); stroke-width: 6; stroke-linecap: round;\n transition: stroke-dasharray 0.6s ease;\n}\n.ka-circle.ka-haut .ka-circle-arc { stroke: var(--green, #256d43); }\n.ka-circle.ka-bon .ka-circle-arc { stroke: var(--accent); }\n.ka-circle.ka-moyen .ka-circle-arc { stroke: var(--orange, #d97a2b); }\n.ka-circle.ka-bas .ka-circle-arc { stroke: #b3543f; }\n.ka-circle-val { font: 700 17px var(--font-body); fill: var(--ink); }\n.ka-circle-nom { font-size: 12px; font-weight: 700; color: var(--ink); margin-top: 4px; }\n.ka-circle-label { font-size: 10.5px; color: var(--ink-3); }\n\n.f-kascores h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ka-perso {\n font-size: 12px; font-weight: 600; color: var(--ink-2);\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 10px;\n}\n.ka-detail summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n padding: 6px 0;\n}\n.ka-detail-grille {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px; padding: 8px 0 4px;\n}\n.ka-detail-grille h4 { margin: 0 0 4px; font-size: 12px; color: var(--ink); }\n.ka-detail-grille ul { margin: 0 0 10px; padding-left: 16px; font-size: 12.5px; color: var(--ink-2); }\n.ka-detail-grille li { margin: 2px 0; }\n\n.ka-poids { display: grid; gap: 10px; margin: 14px 0; max-width: 460px; }\n.ka-poids-row {\n display: grid; grid-template-columns: 90px 1fr 48px; align-items: center;\n gap: 12px; font-size: 13px; font-weight: 600; color: var(--ink);\n}\n.ka-poids-row input[type=\"range\"] { accent-color: var(--accent); }\n.ka-poids-row b { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ms-car-price .ka-badge { margin-left: 2px; }\n@media (max-width: 780px) {\n .ms .ka-3d { top: 148px; right: 10px; }\n}\n\n\n/* ---- Registre des loyers (fiche) — bloc citoyen registre-des-loyers.ca ---- */\n.f-rdl .rdl-resume { margin: 2px 0 6px; }\n.f-rdl .rdl-ecart { margin: 0 0 8px; }\n.rdl-rooms { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 10px; }\n.rdl-room { font-size: 13px; color: var(--ink-2); white-space: nowrap; }\n.rdl-room i { font-style: normal; color: var(--ink-3); }\n.rdl-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.rdl-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line, #e6e4df);\n vertical-align: top; }\n.rdl-table .rdl-addr { max-width: 46%; }\n.rdl-table .rdl-prix { font-weight: 600; white-space: nowrap; }\n.rdl-table .rdl-date { white-space: nowrap; color: var(--ink-2); }\n.rdl-table .rdl-dist { color: var(--ink-3); white-space: nowrap; text-align: right; }\n.rdl-room-on { background: var(--sand, #f4efe7); border-radius: 6px; padding: 2px 8px; }\n\n\n/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */\n.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }\n.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n font-size: 12.5px; font-weight: 700; }\n.zi-eleve { background: #fde8e8; color: #a12622; }\n.zi-modere { background: #fdf3e0; color: #8a5a00; }\n.zi-present { background: #fdf3e0; color: #8a5a00; }\n.zi-ok { background: #e7f4ea; color: #1e6b34; }\n.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n\n\n/* ---- Historique TAL (fiche) — décisions SOQUIJ à l'adresse ---- */\n.f-tal .tal-pending, .f-tal .tal-ok, .f-tal .tal-resume { margin: 2px 0 8px; }\n.tal-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.tal-liste li { padding: 8px 0; border-top: 1px solid var(--line, #e6e4df); }\n.tal-ligne { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }\n.tal-date { font-weight: 600; color: var(--ink-2); }\n.tal-part { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: var(--sand, #f0ede8); color: var(--ink-2); }\n.tal-part-locateur { background: #fdf3e0; color: #8a5a00; }\n.tal-verdict { font-size: 12px; color: var(--ink-3); }\n.tal-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 5px 0 3px; }\n.tal-tag { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: #fde8e8; color: #a12622; }\n.tal-ref { font-size: 12.5px; color: var(--ink-3); }\n\n\n/* ---- Registre des loyers v2 : tuiles, bande de position, barres ---- */\n.rdl-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 12px; }\n.rdl-kpi { flex: 1 1 90px; min-width: 90px; background: var(--surface-2, #f7f5f1);\n border-radius: 10px; padding: 10px 12px; }\n.rdl-kpi-v { display: block; font-size: 20px; font-weight: 700;\n letter-spacing: -0.02em; color: var(--navy); }\n.rdl-kpi-l { display: block; font-size: 11px; line-height: 1.35;\n color: var(--ink-3); margin-top: 2px; }\n.rdl-strip { width: 100%; height: auto; display: block; margin: 2px 0 4px; }\n.rdl-strip-track { fill: var(--surface-2, #f0ede8); }\n.rdl-strip-box { fill: var(--accent-soft, #fff1e6);\n stroke: var(--accent, #ff6a00); stroke-width: 1; }\n.rdl-strip-med { stroke: var(--ink-2, #4c4a45); stroke-width: 2; }\n.rdl-strip-me { stroke: var(--accent-deep, #cc5500); stroke-width: 2; }\n.rdl-strip-me-dot { fill: var(--accent-deep, #cc5500);\n stroke: var(--surface, #fff); stroke-width: 2; }\n.rdl-strip-lbl, .rdl-strip-axis { font-size: 10.5px; fill: var(--ink-3); }\n.rdl-strip-me-lbl { font-size: 11px; font-weight: 700;\n fill: var(--accent-deep, #cc5500); }\n.rdl-bars { width: 100%; height: auto; display: block; margin: 6px 0 2px; }\n.rdl-bar { fill: var(--accent, #ff6a00); opacity: 0.55; }\n.rdl-bar-on { fill: var(--accent-deep, #cc5500); opacity: 1; }\n.rdl-bar-val { font-size: 10.5px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat { font-size: 11px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat-on { font-weight: 700; fill: var(--ink, #1c1b18); }\n.rdl-bar-n { font-size: 9.5px; fill: var(--ink-3, #8a877f); }\n.rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }\n.rdl-ecart { margin: 4px 0 8px; }\n.rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }\n\n\n/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */\n.q-crime { width: 100%; }\n.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }\n.q-crime-cats li { display: flex; align-items: center; gap: 8px;\n padding: 3px 0; font-size: 12.5px; }\n.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.q-crime-barre i { display: block; height: 100%; border-radius: 5px;\n background: var(--accent, #ff6a00); opacity: 0.6; }\n.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; }\n.q-crime-n small { font-weight: 400; color: var(--ink-3); }\n.q-crime-src { margin-top: 8px; }\n\n\n/* ---- Qualité de l'air (RSQAQ) + Essence à proximité ---- */\n.air-liste { list-style: none; margin: 10px 0 0; padding: 0; }\n.air-liste li { display: flex; align-items: center; gap: 8px;\n padding: 4px 0; font-size: 12.5px; }\n.air-nom { flex: 0 0 44%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.air-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.air-barre i { display: block; height: 100%; border-radius: 5px;\n background: #4d9e64; }\n.air-barre i.air-sur { background: #c96a1f; }\n.air-val { flex: 0 0 34%; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.air-val small { font-weight: 400; color: var(--ink-3); }\n.air-val .air-ref { display: block; font-size: 10px; }\n.gaz-head th { text-align: left; font-size: 11px; color: var(--ink-3);\n font-weight: 600; padding: 2px 8px 2px 0; }\n.gaz-best { display: inline-block; margin-left: 6px; padding: 1px 7px;\n border-radius: 999px; background: #e7f4ea; color: #1e6b34;\n font-size: 10.5px; font-weight: 700; }\n.gaz-adr { display: block; font-size: 11px; color: var(--ink-3); }\n\n\n/* ---- Commerces et transport (fiche) ---- */\n.cm-grille { list-style: none; margin: 6px 0 0; padding: 0;\n display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }\n@media (max-width: 560px) { .cm-grille { grid-template-columns: 1fr; } }\n.cm-item { display: flex; align-items: center; gap: 9px; padding: 5px 0;\n border-bottom: 1px solid var(--line, #eeece7); min-width: 0; }\n.cm-ico { flex: 0 0 28px; }\n.cm-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.cm-nom { font-size: 13px; font-weight: 600; color: var(--ink, #222);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-poi { font-size: 11px; color: var(--ink-3, #8a877f);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-dist { flex: 0 0 auto; font-size: 13px; font-weight: 700;\n color: var(--ink-2, #4c4a45); white-space: nowrap; }\n\n\n/* ---- Coût d'électricité (Hydro-Québec) ---- */\n.hydro-intro { margin: 2px 0 12px; color: var(--ink-2, #4c4a45); }\n.hydro-btn { appearance: none; border: 0; border-radius: 10px; cursor: pointer;\n background: var(--accent, #2b7a3b); color: #fff; font-weight: 700;\n font-size: 14px; padding: 11px 18px; }\n.hydro-btn:disabled { opacity: 0.6; cursor: progress; }\n.hydro-montant { font-size: 26px; font-weight: 800; letter-spacing: -0.02em;\n color: var(--navy, #1c1b18); display: flex; align-items: baseline;\n gap: 10px; flex-wrap: wrap; }\n.hydro-montant small { font-size: 14px; font-weight: 500; color: var(--ink-3); }\n.hydro-an { font-size: 14px; font-weight: 600; color: var(--ink-2, #4c4a45); }\n.hydro-vide { color: var(--ink-3, #8a877f); }\n\n\n/* ============================================================================\n PASSE DE DESIGN v3 — « éditorial sharp » appliqué à toute la fiche\n Blocs = cartes encre (bordure 1,5 px + ombre décalée douce), titres à\n marqueur accent + filet, tuiles KPI blanches à ombre dure, tableaux zébrés,\n barres en dégradé, pastilles unifiées (mono, point d'état, liseré).\n Aucune propriété `order` (règle Groupe Ka : ordre DOM = ordre visuel).\n ========================================================================== */\n\n/* --- Blocs de la fiche : cartes posées sur le papier --- */\n.f-bloc:not(.f-galerie):not(.f-hero) {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-card);\n padding: 18px 20px 16px;\n box-shadow: var(--shadow-off-soft);\n}\n@media (max-width: 640px) {\n .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n}\n\n/* --- Titres de section : carré accent + filet éditorial --- */\n.f-bloc h2, .quartier h2 {\n display: flex; align-items: center; gap: 10px;\n font-size: 19px; letter-spacing: -0.015em; margin-bottom: 14px;\n}\n.f-bloc h2::before, .quartier h2::before {\n content: \"\"; flex: 0 0 9px; width: 9px; height: 9px;\n background: var(--accent); border: 1.5px solid var(--ink);\n box-shadow: 2px 2px 0 var(--ink);\n}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }\n.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--line);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n","structuredPatch":[{"oldStart":2161,"oldLines":7,"newStart":2161,"newLines":7,"lines":[" /* --- Historique Lou-Ka : timeline --- */"," .hl-timeline {"," list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;","- border-left: 2px solid var(--line);","+ border-left: 2px solid var(--ink);"," display: flex; flex-direction: column; gap: 8px;"," }"," .hl-item { position: relative; font-size: 13px; color: var(--ink-2); }"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMd4hZN8aer28XBLFco","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BptjDdsZPgdZj5Mnen7PhC","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","old_string":"/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}","new_string":"/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 30px;\n line-height: 1; color: var(--ink);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 8px 12px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2672,"cache_read_input_tokens":104805,"cache_creation":{"ephemeral_5m_input_tokens":2672,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9daef03a-69a8-4a8a-8c7d-1e0c1931e3d3"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BptjDdsZPgdZj5Mnen7PhC","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"707e9cae-bf4f-4d12-96b1-52fafd857c64","timestamp":"2026-08-24T22:24:17.329Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","oldString":"/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}","newString":"/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 30px;\n line-height: 1; color: var(--ink);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 8px 12px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}","originalFile":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }\n\n/* Garde-fou : jamais de défilement horizontal de la page, même transitoire\n pendant le chargement (bandeau défilant, polices) — clip ne casse pas\n position: sticky, contrairement à hidden. */\nhtml, body { overflow-x: clip; }\n\n/* ================= Header ================= */\n.header {\n position: sticky; top: 0; z-index: var(--z-header, 500);\n background: var(--paper);\n border-bottom: 1px solid var(--line);\n}\n.header-inner { display: flex; align-items: center; gap: 20px; height: 64px; }\n.brand {\n font-family: var(--font-display); font-weight: 700; font-size: 25px;\n letter-spacing: -0.03em; display: flex; align-items: center; line-height: 1;\n color: var(--navy);\n}\n.brand .logo-ico { margin-right: 12px; flex: none; } /* ≈ 40 % de la largeur de l'icône */\n.brand .ka { color: var(--orange); }\n.brand-tag {\n font-size: 11px; font-weight: 500; color: var(--ink-3);\n letter-spacing: 0.02em; margin-left: 12px;\n}\n@media (max-width: 900px) { .brand-tag { display: none; } }\n.nav { margin-left: auto; display: flex; gap: 4px; }\n.nav a {\n padding: 9px 16px; border-radius: var(--r-pill); font-weight: 600; font-size: 14px;\n color: var(--navy); transition: all 0.15s ease; min-height: 40px;\n display: inline-flex; align-items: center;\n}\n.nav a:hover { color: var(--orange); background: var(--orange-soft); }\n.nav a.active { background: var(--navy); color: var(--white); }\n\n/* ================= Ticker ================= */\n.ticker {\n background: var(--navy); color: rgba(255, 255, 255, 0.85); overflow: hidden;\n font-size: 11.5px; font-weight: 500; letter-spacing: 0.08em;\n text-transform: uppercase; padding: 7px 0; white-space: nowrap;\n}\n.ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }\n.ticker span { padding: 0 26px; position: relative; }\n.ticker span::after { content: \"◆\"; position: absolute; right: -6px; color: var(--orange); opacity: 0.8; font-size: 8px; top: 3px; }\n@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) {\n .ticker-track { animation: none; }\n * { transition-duration: 0.01ms !important; }\n}\n\n/* ================= Hero ================= */\n.hero { padding: 58px 0 22px; }\n@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n.hero h1 {\n font-size: clamp(36px, 5.4vw, 56px); font-weight: 700; line-height: 1.06;\n letter-spacing: -0.03em; max-width: 900px; margin-top: 14px; color: var(--navy);\n}\n.hero h1 .outline { color: var(--navy); }\n.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }\n.stat-chip {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 8px 16px; font-size: 12.5px; font-weight: 500;\n color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n box-shadow: var(--shadow-flat);\n}\n.stat-chip b { color: var(--navy); font-weight: 700; }\n.stat-chip .pulse {\n width: 8px; height: 8px; border-radius: 50%; background: var(--orange);\n box-shadow: 0 0 0 4px var(--orange-soft); animation: pulse 2.4s ease infinite;\n}\n@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 106, 0, 0.18); } }\n\n/* ================= Rangée d'avantages ================= */\n.benefits {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px; margin-top: 30px;\n}\n.benefit { display: flex; align-items: center; gap: 12px; }\n.benefit-ico {\n width: 42px; height: 42px; border-radius: var(--r-pill); flex: none;\n display: flex; align-items: center; justify-content: center;\n background: var(--orange-soft); color: var(--orange);\n}\n.benefit-ico.blue { background: var(--blue-soft); color: var(--blue); }\n.benefit-label {\n font-size: 11.5px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--navy);\n}\n@media (max-width: 640px) {\n .benefits { grid-template-columns: 1fr 1fr; gap: 12px; }\n}\n\n/* ================= Filter bar ================= */\n.filterbar {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 14px; margin: 30px 0 6px;\n display: flex; flex-direction: column; gap: 0; min-width: 0;\n}\n\n/* — rangée principale — */\n.f-primary { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; min-width: 0; }\n.f-search {\n flex: 1 1 220px; min-width: 0; max-width: 440px; align-self: center;\n display: flex; align-items: center; gap: 8px;\n border: 1px solid transparent; border-radius: var(--r-pill); background: var(--grey);\n padding: 0 15px; min-height: 42px; color: var(--ink-2);\n transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;\n}\n.f-search:focus-within {\n background: var(--white); border-color: var(--blue);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n.f-search input {\n border: none; background: none; outline: none; flex: 1; min-width: 0;\n font-size: 14px; color: var(--ink); font-family: inherit;\n}\n.f-clear {\n border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n width: 20px; height: 20px; font-size: 10px; cursor: pointer; flex: none;\n display: grid; place-items: center;\n}\n.f-ctl {\n flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center;\n gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n padding: 7px 14px 6px; min-height: 52px; cursor: pointer;\n transition: box-shadow 0.15s ease, border-color 0.15s ease;\n}\n.f-ctl:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.f-ctl > span {\n font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-ctl select {\n border: none; background: transparent; outline: none; font-family: var(--font-display);\n font-weight: 700; font-size: 14.5px; color: var(--navy); cursor: pointer;\n appearance: none; -webkit-appearance: none; padding-right: 16px; min-width: 0; max-width: 170px;\n text-overflow: ellipsis;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right center;\n}\n.range-pair { display: flex; align-items: center; gap: 4px; }\n.range-pair select { max-width: 86px; }\n.range-sep { color: var(--ink-3); font-size: 11px; }\n.f-more {\n flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n transition: all 0.13s ease;\n}\n.f-more:hover { background: var(--blue-soft); border-color: var(--blue); color: var(--blue); }\n.f-more.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* — panneau avancé — */\n.f-adv {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 22px;\n border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n animation: adv-in 0.18s ease;\n}\n@keyframes adv-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }\n.f-group { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.f-group > label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-group-end { justify-content: flex-end; }\n.f-group .btn:disabled { opacity: 0.4; cursor: default; }\n.f-native {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 10px 30px 10px 14px; font-size: 14px; color: var(--ink); outline: none;\n font-family: inherit; min-height: 42px; width: 100%; min-width: 0;\n appearance: none; -webkit-appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center;\n}\n.f-native:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); background: var(--white); }\n\n/* segments (pilules soudées) */\n.seg { display: inline-flex; flex-wrap: wrap; row-gap: 6px; }\n.seg button {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink-2);\n padding: 8px 14px; font-family: var(--font-display); font-weight: 600; font-size: 13px;\n cursor: pointer; margin-left: -1px; white-space: nowrap; min-height: 38px;\n transition: all 0.12s ease;\n}\n.seg button:first-child { border-radius: var(--r-pill) 0 0 var(--r-pill); margin-left: 0; padding-left: 16px; }\n.seg button:last-child { border-radius: 0 var(--r-pill) var(--r-pill) 0; padding-right: 16px; }\n.seg button:hover { background: var(--blue-soft); color: var(--blue); }\n.seg button.on { background: var(--blue); border-color: var(--blue); color: var(--white); position: relative; z-index: 1; }\n\n/* pastilles de filtres actifs */\n.pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }\n.pill {\n display: inline-flex; align-items: center; gap: 7px;\n border: 1px solid rgba(28, 92, 65, 0.3); background: var(--blue-soft); color: var(--blue);\n border-radius: var(--r-pill); padding: 6px 14px; font-size: 12.5px; font-weight: 600;\n cursor: pointer; transition: all 0.13s ease;\n}\n.pill:hover { background: var(--blue); color: var(--white); }\n.pill-x { font-size: 10px; opacity: 0.65; }\n.pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }\n.pill-clear:hover { background: var(--danger); color: #fff; }\n.chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 4px 4px; flex: none; }\n.link-btn {\n background: none; border: none; padding: 0; color: var(--blue); font: inherit;\n text-decoration: underline; cursor: pointer;\n}\n.field { display: flex; flex-direction: column; gap: 5px; }\n.field label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); padding-left: 2px;\n}\n.field input, .field select {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 11px 14px; font-size: 15px; color: var(--ink); outline: none; font-family: inherit;\n transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; min-height: 44px;\n appearance: none; -webkit-appearance: none;\n}\n.field select {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px;\n}\n.field input:focus, .field select:focus {\n border-color: var(--blue); background: var(--white);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n/* .btn / .btn-primary / .btn-ghost : fournis par tokens.css (ka-ui). */\n\n/* ================= Chips ================= */\n.chips { display: flex; gap: 8px; margin: 16px 0 4px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }\n.chips::-webkit-scrollbar { display: none; }\n.chip {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink);\n border-radius: var(--r-pill); padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer;\n font-family: var(--font-display); white-space: nowrap; min-height: 40px;\n transition: transform 0.13s, box-shadow 0.13s, background 0.13s, color 0.13s;\n}\n.chip:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.chip.on { background: var(--ink); border-color: var(--ink); color: var(--accent); box-shadow: 3px 3px 0 var(--accent); }\n\n/* ================= Results ================= */\n.results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }\n.results-head h2 { font-size: 22px; letter-spacing: -0.02em; color: var(--navy); }\n.results-head span { color: var(--ink-3); font-size: 12.5px; font-weight: 500; }\n.grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));\n gap: 22px; padding-bottom: 70px;\n}\n@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n\n.card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-flat);\n transition: transform 0.16s ease, box-shadow 0.16s ease;\n}\n.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-off); }\n.card:focus-visible { outline: 3px solid rgba(28, 92, 65, 0.4); outline-offset: 2px; }\n.card-img { position: relative; aspect-ratio: 16/10.5; background: var(--grey); overflow: hidden; }\n.card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }\n.card:hover .card-img img { transform: scale(1.05); }\n.card-img .noimg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--ink-3); font-size: 34px; }\n.badge {\n position: absolute; top: 12px; left: 12px; border-radius: var(--r-pill); padding: 4px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: 0.04em;\n background: rgba(255, 255, 255, 0.94); color: var(--navy);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.badge.type { background: var(--orange); color: var(--white); }\n.badge.right { left: auto; right: 12px; background: rgba(255, 255, 255, 0.92); }\n.card-body { padding: 16px 18px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\n.card-price { font-family: var(--font-display); font-weight: 700; font-size: 21px; letter-spacing: -0.02em; color: var(--navy); }\n.card-price small { font-weight: 500; color: var(--ink-3); font-size: 11.5px; }\n.card-title { font-weight: 600; font-size: 14.5px; color: var(--ink); }\n.card-meta { color: var(--ink-3); font-size: 12.5px; display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }\n.card-meta .sep { width: 4px; height: 4px; background: var(--orange); border-radius: 50%; }\n.card-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.source-tag {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--blue); background: var(--blue-soft);\n border-radius: var(--r-pill); padding: 3px 10px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;\n}\n.avail { font-size: 11.5px; color: var(--ink-2); font-weight: 500; text-align: right; }\n\n/* ================= Skeletons ================= */\n@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }\n.skel { border-radius: var(--r-card); border: 1.5px solid var(--ink); overflow: hidden; background: var(--surface); }\n.skel .sk-img, .skel .sk-line {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.skel .sk-img { aspect-ratio: 16/10.5; }\n.skel .sk-line { height: 14px; border-radius: 4px; margin: 12px 16px; }\n.skel .sk-line.short { width: 45%; }\n\n/* ================= Empty / error ================= */\n.notice { text-align: center; padding: 72px 24px; color: var(--ink-2); }\n.notice .big { font-size: 44px; margin-bottom: 10px; }\n.notice h2 { color: var(--navy); }\n\n/* ================= Detail page ================= */\n.detail { padding: 30px 0 90px; }\n.crumbs {\n font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;\n color: var(--ink-3); margin-bottom: 20px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;\n}\n.crumbs a { border-bottom: 1.5px solid transparent; }\n.crumbs a:hover { color: var(--blue); border-color: var(--blue); }\n.detail-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n/* les enfants de grille ont min-width:auto par défaut : un contenu large\n (grille du quartier) déborderait de l'écran mobile sans ceci */\n.detail-grid > * { min-width: 0; }\n\n.gallery { display: flex; flex-direction: column; gap: 10px; }\n.gallery-main {\n border-radius: var(--r-card); overflow: hidden; border: 1.5px solid var(--ink);\n aspect-ratio: 16/10; background: var(--grey); cursor: zoom-in; box-shadow: var(--shadow-off-soft);\n}\n.gallery-main img { width: 100%; height: 100%; object-fit: cover; }\n.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; }\n.thumbs button {\n border: 2px solid var(--line); border-radius: 12px; overflow: hidden; padding: 0; cursor: pointer;\n aspect-ratio: 4/3; background: var(--grey); transition: border-color 0.12s ease, transform 0.12s ease;\n}\n.thumbs button:hover { transform: translateY(-2px); }\n.thumbs button.on { border-color: var(--orange); }\n.thumbs img { width: 100%; height: 100%; object-fit: cover; }\n\n.panel {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px; position: sticky; top: 120px;\n}\n@media (max-width: 900px) { .panel { position: static; } }\n.panel .price { font-family: var(--font-display); font-size: 34px; font-weight: 700; letter-spacing: -0.03em; color: var(--navy); }\n.panel .price small { font-size: 12.5px; color: var(--ink-3); font-weight: 500; }\n.panel h1 { font-size: 22px; margin: 8px 0 2px; }\n.panel .loc { color: var(--ink-2); font-size: 14px; }\n.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n/* .klabel : fourni par tokens.css (ka-ui). */\n.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n.amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }\n.amenity {\n background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 600;\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-pill); padding: 5px 13px;\n}\n.cta {\n display: block; text-align: center; background: var(--orange); color: var(--white);\n font-weight: 600; font-family: var(--font-display); border-radius: var(--r-pill);\n padding: 15px; min-height: 48px;\n box-shadow: 0 8px 22px rgba(255, 106, 0, 0.34); transition: all 0.14s ease;\n}\n.cta:hover { background: var(--orange-deep); }\n.cta:active { transform: translateY(1px); }\n.panel .fine { font-size: 11.5px; color: var(--ink-3); margin-top: 14px; text-align: center; }\n\n/* ================= Lightbox ================= */\n.lightbox {\n position: fixed; inset: 0; background: rgba(20, 24, 20, 0.94); z-index: var(--z-modal, 900);\n display: flex; align-items: center; justify-content: center; cursor: zoom-out;\n padding: max(16px, env(safe-area-inset-top)) 16px;\n}\n.lightbox img { max-width: 94vw; max-height: 90vh; border-radius: 12px; }\n\n/* Lightbox v2 : balayage entre photos (scroll-snap) + pincement pour zoomer */\n.lightbox-v2 { padding: 0; cursor: default; display: block; }\n.lightbox-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%;\n -webkit-overflow-scrolling: touch; scrollbar-width: none;\n touch-action: pan-x pinch-zoom;\n}\n.lightbox-track::-webkit-scrollbar { display: none; }\n.lightbox-cell {\n flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center;\n display: flex; align-items: center; justify-content: center;\n padding: max(52px, env(safe-area-inset-top)) 10px max(24px, env(safe-area-inset-bottom));\n overflow: hidden;\n}\n.lightbox-cell img {\n max-width: 100%; max-height: 100%; border-radius: 10px;\n user-select: none; -webkit-user-drag: none; will-change: transform;\n}\n.lightbox-close {\n position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.4);\n background: rgba(20, 24, 20, 0.6); color: #fff; font-size: 18px; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.lightbox-count {\n position: fixed; left: 50%; transform: translateX(-50%); right: auto;\n top: max(22px, env(safe-area-inset-top)); bottom: auto; z-index: 3;\n}\n.lightbox-v2 .carousel-nav { position: fixed; z-index: 3; }\n\n/* visuel de secours (SmartImg) directement sous .carousel (galerie vide) */\n.carousel > img { display: block; width: 100%; aspect-ratio: 16/11; object-fit: cover; }\n\n/* ================= Sources page ================= */\n.sources { padding: 44px 0 90px; }\n.sources h1 { font-size: clamp(28px, 4vw, 40px); margin: 10px 0 6px; color: var(--navy); }\n.sources .sub { color: var(--ink-2); margin-bottom: 30px; max-width: 700px; }\n.src-wrap { overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); background: var(--surface); }\n.src-table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 720px; }\n.src-table th {\n text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: 0.1em; color: var(--ink-3); padding: 14px 18px;\n border-bottom: 1px solid var(--line-strong); background: var(--grey);\n}\n.src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }\n.src-table tr:last-child td { border-bottom: none; }\n.src-table tr:hover td { background: var(--navy-soft); }\n.src-table a { color: var(--blue); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.pill { display: inline-block; border-radius: var(--r-pill); padding: 3px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }\n.pill.ok { background: var(--blue-soft); color: var(--blue); }\n.pill.todo { background: var(--orange-soft); color: var(--orange-deep); }\n.count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--navy); }\n\n/* ================= Page Statistiques ================= */\n.stats-page { padding: 44px 0 90px; }\n.stats-title { font-size: clamp(30px, 4.6vw, 46px); margin: 10px 0 6px; color: var(--navy); }\n.stats-page .sub { color: var(--ink-2); max-width: 640px; margin-bottom: 30px; }\n\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 30px; }\n.tile {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n padding: 18px 20px; box-shadow: var(--shadow-flat);\n}\n.tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--navy); }\n.tile-k { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }\n.hero-tile { background: var(--navy); color: var(--white); border-color: var(--navy); }\n.hero-tile .tile-v { color: var(--white); }\n.hero-tile .tile-k { color: rgba(255, 255, 255, 0.6); }\n\n.viz-card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px 28px 20px; margin-bottom: 22px;\n}\n.viz-card h2 { font-size: 19px; letter-spacing: -0.01em; color: var(--navy); }\n.viz-sub { font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 4px 0 20px; }\n.viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n\n/* Histogramme (mono-série, bleu vif) */\n.histo { display: flex; align-items: stretch; gap: 2px; height: 240px; }\n.histo-col { flex: 1; display: flex; flex-direction: column; min-width: 0; cursor: default; }\n.histo-bar-zone { flex: 1; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line-strong); }\n.histo-bar {\n width: 100%; background: var(--blue); border-radius: 6px 6px 0 0;\n min-height: 2px; transition: background 0.12s ease;\n}\n.histo-col:hover .histo-bar { background: var(--orange); }\n.histo-x { font-size: 9.5px; color: var(--ink-3); text-align: left; height: 18px; padding-top: 5px; overflow: visible; white-space: nowrap; }\n\n/* Barres horizontales (mono-série) */\n.hbars { display: flex; flex-direction: column; gap: 7px; }\n.hbar-row {\n display: grid; grid-template-columns: minmax(96px, 170px) 1fr auto;\n gap: 12px; align-items: center; min-height: 26px; cursor: default;\n}\n.hbar-label { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.hbar-label a { border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.hbar-label a:hover { color: var(--blue); }\n.hbar-track { background: var(--grey); border-radius: var(--r-pill); height: 18px; overflow: hidden; }\n.hbar-fill {\n display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill);\n min-width: 2px; transition: background 0.12s ease;\n}\n.hbar-row:hover .hbar-fill { background: var(--orange); }\n.hbar-value { font-size: 11.5px; font-weight: 700; white-space: nowrap; color: var(--navy); }\n.hbar-value em { font-style: normal; font-weight: 500; color: var(--ink-3); }\n@media (max-width: 640px) {\n .hbar-row { grid-template-columns: minmax(80px, 110px) 1fr auto; gap: 8px; }\n .hbar-value em { display: none; }\n .viz-card { padding: 20px 16px 14px; }\n .histo { height: 170px; }\n}\n\n/* Infobulle */\n.viz-tip {\n position: fixed; z-index: var(--z-toast, 950); pointer-events: none; max-width: 180px;\n background: var(--navy); color: var(--white); border-radius: 12px;\n padding: 9px 12px; font-size: 12px; line-height: 1.5;\n box-shadow: 0 8px 24px rgba(20, 24, 20, 0.35);\n}\n.viz-tip-title { font-family: var(--font-display); font-weight: 700; color: var(--orange); margin-bottom: 2px; }\n\n/* Vue tableau repliable (accessibilité / relief) */\n.viz-table { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 10px; }\n.viz-table summary {\n cursor: pointer; font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em; color: var(--blue);\n}\n.viz-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }\n.viz-table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line-strong); }\n.viz-table td { padding: 6px 10px; border-bottom: 1px solid var(--line); }\n\n.stats-foot { font-size: 11.5px; color: var(--ink-3); letter-spacing: 0.02em; margin-top: 6px; }\n\n/* ================= Mobile : feuille de filtres + FAB ================= */\n.sheet-head { display: none; }\n.sheet-apply { display: none; }\n.sheet-backdrop { display: none; }\n.fab { display: none; }\n\n@media (max-width: 640px) {\n /* En-tête compact */\n .header-inner { height: 56px; }\n .brand { font-size: 21px; }\n .nav a { padding: 8px 13px; font-size: 13.5px; }\n .ticker { font-size: 10.5px; padding: 6px 0; }\n\n /* Héro resserré + stats en rangée défilante */\n .hero h1 { font-size: clamp(30px, 8.6vw, 42px); }\n .hero p.lede { font-size: 15px; }\n .stat-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 6px; margin-right: -16px; padding-right: 16px; }\n .stat-row::-webkit-scrollbar { display: none; }\n .stat-chip { flex: 0 0 auto; white-space: nowrap; }\n\n /* La barre de filtres devient une feuille coulissante (bottom sheet) */\n .filterbar { display: none; }\n .filterbar.open .f-primary { flex-direction: column; }\n .filterbar.open .f-search { align-self: stretch; max-width: none; min-height: 46px; }\n .filterbar.open .f-search input { font-size: 16px; } /* anti-zoom iOS */\n .filterbar.open .f-ctl select { max-width: none; width: 100%; }\n .filterbar.open .f-more { display: none; }\n .filterbar.open .f-adv { margin-top: 4px; }\n .filterbar.open {\n display: flex; flex-direction: column; gap: 12px;\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);\n margin: 0; border-radius: 24px 24px 0 0; border: none;\n max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;\n padding: 16px 18px calc(18px + env(safe-area-inset-bottom));\n box-shadow: 0 -16px 48px rgba(20, 24, 20, 0.28);\n animation: sheet-up 0.22s ease;\n }\n @keyframes sheet-up { from { transform: translateY(30%); opacity: 0.4; } to { transform: none; opacity: 1; } }\n .filterbar.open .sheet-head {\n display: flex; justify-content: space-between; align-items: center;\n font-family: var(--font-display); font-weight: 700; font-size: 17px;\n letter-spacing: -0.01em; color: var(--navy);\n position: sticky; top: -16px; background: var(--surface); padding: 6px 0 8px;\n border-bottom: 1px solid var(--line); margin-bottom: 2px; z-index: 1;\n }\n .sheet-close {\n border: 1.5px solid var(--ink); background: var(--surface); border-radius: 50%;\n width: 36px; height: 36px; font-size: 15px; cursor: pointer; line-height: 1; color: var(--navy);\n }\n .filterbar.open .sheet-apply { display: block; width: 100%; }\n .sheet-backdrop {\n display: block; position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.45); backdrop-filter: blur(2px);\n }\n\n /* Bouton flottant */\n .fab {\n display: flex; align-items: center; gap: 6px;\n position: fixed; left: 50%; transform: translateX(-50%);\n bottom: calc(74px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: 80;\n background: var(--orange); color: var(--white); border: none;\n border-radius: var(--r-pill); padding: 13px 24px; font-family: var(--font-display);\n font-weight: 600; font-size: 15px; cursor: pointer;\n box-shadow: 0 10px 28px rgba(255, 106, 0, 0.4);\n }\n .fab:active { transform: translateX(-50%) scale(0.97); }\n\n /* Fiche logement : vignettes en bande défilante + panneau non collant */\n .thumbs { display: flex; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }\n .thumbs::-webkit-scrollbar { display: none; }\n .thumbs button { flex: 0 0 96px; }\n .detail { padding-top: 20px; }\n .panel { padding: 20px; }\n .panel .price { font-size: 28px; }\n .results-head { margin-top: 20px; }\n .chips { margin-right: -16px; padding-right: 16px; }\n .notice { padding: 48px 16px; }\n}\n\n/* ================= Barre de navigation inférieure (mobile) ================= */\n.tabbar { display: none; }\n@media (max-width: 760px) {\n .tabbar {\n display: grid; grid-template-columns: repeat(4, 1fr);\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border-top: 1px solid var(--line);\n padding: 6px 4px calc(6px + env(safe-area-inset-bottom));\n }\n .tabbar a {\n display: flex; flex-direction: column; align-items: center; gap: 3px;\n padding: 5px 2px; min-height: 48px; justify-content: center;\n font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n border-radius: var(--r-ctl); transition: color 0.12s ease;\n }\n .tabbar a.active { color: var(--orange); }\n .tabbar a svg { display: block; }\n body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }\n .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }\n}\n\n/* Anti-zoom iOS : les champs doivent faire >= 16px\n (filet global : tokens.css force aussi 16 px au doigt) */\n@media (max-width: 900px) {\n .field input, .field select,\n .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }\n}\n\n/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */\n@media (pointer: coarse) {\n .brand { position: relative; }\n .brand::after { content: \"\"; position: absolute; inset: -8px; }\n}\n\n/* ================= Footer ================= */\n.footer { background: var(--navy); color: rgba(255, 255, 255, 0.72); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; }\n.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 32px; color: var(--white); letter-spacing: -0.03em; margin-bottom: 4px; display: flex; align-items: center; }\n.footer .fbrand .logo-ico { margin-right: 13px; }\n.footer .fbrand .ka { color: var(--orange); }\n.footer .fbaseline { color: rgba(255, 255, 255, 0.65); font-size: 14px; margin-bottom: 16px; }\n.footer .fbaseline .hl { color: var(--orange); font-weight: 600; }\n.footer b { color: var(--white); }\n.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }\n.footer .fmono { font-size: 11.5px; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.42); margin-top: 18px; }\n\n/* --- Carte interactive (mission 4) --------------------------------------- */\n.results-tools { display: flex; align-items: center; gap: 14px; }\n.view-toggle {\n display: inline-flex; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); overflow: hidden; background: var(--surface);\n box-shadow: var(--shadow-flat);\n}\n.view-toggle button {\n border: 0; background: transparent; padding: 7px 16px; cursor: pointer;\n font-size: 12.5px; font-weight: 600; color: var(--ink-2);\n}\n/* zone tactile >= 44 px au doigt (coarse pointer = mobile/tablette) */\n@media (pointer: coarse) {\n .view-toggle button { min-height: 44px; padding: 10px 18px; }\n}\n.view-toggle button + button { border-left: 1px solid var(--line); }\n.view-toggle button.on { background: var(--blue); color: var(--white); }\n\n.map-split {\n display: grid; grid-template-columns: minmax(300px, 400px) 1fr; gap: 18px;\n height: calc(100dvh - 170px); min-height: 420px;\n}\n.map-list {\n overflow-y: auto; display: flex; flex-direction: column; gap: 14px;\n padding-right: 4px; scrollbar-width: thin;\n}\n.map-list .card { margin: 0; flex: 0 0 auto; }\n.mapview {\n width: 100%; height: 100%; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.map-loading {\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-3); font-size: 13px;\n}\n\n/* mobile d'abord : carte plein écran, liste masquée (bascule via l'onglet) */\n@media (max-width: 780px) {\n .map-split { grid-template-columns: 1fr; height: calc(100dvh - 210px); }\n .map-list { display: none; }\n .results-tools { width: 100%; justify-content: space-between; }\n}\n\n/* popup MapLibre au style Lou-Ka */\n.maplibregl-popup-content {\n padding: 0; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); box-shadow: var(--shadow-off);\n font-family: var(--font-body);\n}\n.maplibregl-popup-close-button {\n font-size: 18px; padding: 2px 8px; color: var(--white); z-index: 2;\n text-shadow: 0 0 4px rgba(20, 24, 20, 0.8);\n}\n.mv-pop img, .mv-noimg { width: 100%; height: 130px; object-fit: cover; }\n.mv-noimg { display: flex; align-items: center; justify-content: center;\n font-size: 34px; background: var(--grey); }\n.mv-pop-body { padding: 10px 12px 12px; }\n.mv-pop-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--navy); }\n.mv-pop-price small { font-weight: 400; color: var(--ink-3); font-size: 12px; }\n.mv-pop-title { font-size: 13px; color: var(--ink-2); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mv-pop-meta { font-size: 11.5px; color: var(--ink-2); margin-top: 6px; }\n.mv-pop-src { font-size: 10px; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }\n.mv-pop-cta {\n display: block; margin-top: 10px; padding: 9px 10px; text-align: center;\n background: var(--orange); color: var(--white); border-radius: var(--r-pill);\n font-weight: 600; font-size: 13px;\n}\n.mv-pop-cta:hover { background: var(--orange-deep); }\n\n/* --- Commodités de proximité (fiche) -------------------------------------- */\n.poi-list {\n list-style: none; margin: 0; padding: 0;\n display: flex; flex-direction: column; gap: 2px;\n}\n.poi-list li {\n display: flex; align-items: center; gap: 9px;\n padding: 5px 2px; border-bottom: 1px solid var(--line);\n font-size: 13px;\n}\n.poi-list li:last-child { border-bottom: 0; }\n.poi-ico { width: 20px; text-align: center; flex: 0 0 auto; }\n.poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.poi-dist { font-size: 11.5px; color: var(--navy); font-weight: 600; flex: 0 0 auto; }\n\n/* --- Menu déroulant mobile ------------------------------------------------- */\n.menu-btn {\n display: none; position: relative; z-index: 60;\n width: 44px; height: 44px; margin-left: auto;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); cursor: pointer; padding: 0;\n flex-direction: column; align-items: center; justify-content: center; gap: 5px;\n box-shadow: var(--shadow-flat);\n transition: box-shadow 0.15s ease, transform 0.15s ease;\n}\n.menu-btn:active { transform: scale(0.96); }\n.menu-btn span {\n display: block; width: 18px; height: 2px; background: var(--navy);\n border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease;\n}\n.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }\n.menu-btn.open span:nth-child(2) { opacity: 0; }\n.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }\n\n.mobile-menu {\n display: none; position: absolute; left: 0; right: 0; top: 100%;\n background: var(--white); border-bottom: 1px solid var(--line);\n box-shadow: 0 18px 30px rgba(20, 24, 20, 0.14);\n padding: 8px 16px calc(16px + env(safe-area-inset-bottom));\n max-height: 0; overflow: hidden; opacity: 0;\n transition: max-height 0.3s ease, opacity 0.22s ease;\n}\n.mobile-menu { z-index: var(--z-modal, 900); }\n.mobile-menu.open { max-height: 480px; opacity: 1; }\n/* Le drawer vit DANS le header : quand il est ouvert, le header monte au niveau\n modal pour peindre au-dessus du .mm-backdrop (frère du header, overlay 800). */\n.header:has(.mobile-menu.open) { z-index: var(--z-modal, 900); }\n.mm-link {\n display: flex; align-items: center; gap: 12px;\n padding: 15px 10px; min-height: 52px;\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n letter-spacing: -0.02em; border-bottom: 1px solid var(--line);\n color: var(--navy);\n opacity: 0; transform: translateY(-8px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); }\n.mm-link.active { color: var(--orange); }\n.mm-link.active .mm-arrow { opacity: 1; color: var(--orange); }\n.mm-ico { width: 26px; text-align: center; font-size: 17px; }\n.mm-arrow { margin-left: auto; opacity: 0.25; color: var(--orange); transition: opacity 0.15s ease; }\n.mm-link:active { background: var(--orange-soft); border-radius: var(--r-ctl); }\n.mm-foot {\n padding: 12px 10px 4px;\n font-size: 11px; color: var(--ink-3); letter-spacing: 0.02em;\n}\n.mm-backdrop {\n position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px);\n}\n@media (max-width: 760px) {\n .menu-btn { display: flex; }\n .nav { display: none; }\n .mobile-menu { display: block; }\n}\n\n/* --- Bandeau de consentement (témoins) ------------------------------------- */\n.cookie-banner {\n position: fixed; left: 12px; right: 12px;\n bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80;\n animation: cookie-in 0.35s ease;\n}\n@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } }\n.cookie-inner {\n max-width: 720px; margin: 0 auto; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 16px 18px;\n}\n.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }\n.cookie-text b { color: var(--navy); }\n.cookie-text a { text-decoration: underline; text-underline-offset: 2px; }\n.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n.cookie-opt {\n display: flex; align-items: flex-start; gap: 10px; font-size: 13px;\n color: var(--ink-2); cursor: pointer;\n}\n.cookie-opt input { margin-top: 2px; accent-color: var(--orange); width: 16px; height: 16px; }\n.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; }\n@media (max-width: 640px) {\n .cookie-actions { justify-content: stretch; }\n .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; }\n}\n\n/* --- Page confidentialité + lien pied de page ------------------------------- */\n.legal { padding: 48px 0 70px; max-width: 760px; }\n.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; color: var(--navy); }\n.legal section { margin-top: 26px; }\n.legal h3 { font-size: 18px; margin-bottom: 8px; }\n.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }\n.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }\n.flink {\n border: 0; background: none; padding: 0; cursor: pointer;\n color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px;\n}\n\n/* --- Section « Le quartier » (fiche) --------------------------------------- */\n.quartier { margin-top: 34px; }\n.quartier h2 { font-size: 24px; letter-spacing: -0.02em; color: var(--navy); }\n.q-sub { color: var(--ink-3); font-size: 13px; margin: 4px 0 16px; }\n.q-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;\n}\n@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n.q-cell {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 12px 14px; box-shadow: var(--shadow-flat);\n}\n.q-val { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.q-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px; }\n.q-prox { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }\n.q-bar { display: flex; align-items: center; gap: 10px; font-size: 13px; }\n.q-bar-label { flex: 0 0 150px; color: var(--ink-2); }\n@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n.q-bar-track {\n flex: 1; height: 10px; background: var(--grey);\n border-radius: var(--r-pill); overflow: hidden;\n}\n.q-bar-fill { display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill); }\n.q-bar-num { flex: 0 0 30px; text-align: right;\n font-size: 11.5px; font-weight: 700; color: var(--navy); }\n.q-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.q-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 7px 14px; font-size: 12.5px; background: var(--surface);\n}\n.q-badge.cool { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.q-badge.hot { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.quartier .fine { margin-top: 10px; }\n\n/* --- Fiche v2 (mobile-first) ------------------------------------------------ */\n/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka\n « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`\n / `display:contents` pour réordonner — les blocs sans `order` retombaient à\n order:0 et passaient devant la galerie sur mobile. Mobile : colonnes\n empilées ; desktop : grid 2 colonnes (logement | synthèse). */\n.fiche { display: flex; flex-direction: column; gap: 22px; }\n.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n@media (min-width: 900px) {\n .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n .f-col { gap: 26px; }\n}\n.f-bloc { min-width: 0; }\n.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n.f-bloc:empty { display: none; }\n\n/* galerie à balayage natif */\n.carousel { position: relative; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); background: var(--grey); box-shadow: var(--shadow-off-soft); }\n.carousel-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch; scrollbar-width: none; aspect-ratio: 16/11;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track img {\n flex: 0 0 100%; width: 100%; object-fit: cover; scroll-snap-align: center;\n cursor: zoom-in;\n}\n.carousel-empty { display: flex; align-items: center; justify-content: center;\n aspect-ratio: 16/11; font-size: 48px; }\n.carousel-count {\n position: absolute; right: 12px; bottom: 12px; z-index: 2;\n background: rgba(20, 24, 20, 0.78); color: var(--white);\n font-size: 12px; font-weight: 600;\n padding: 4px 11px; border-radius: var(--r-pill);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.carousel-nav {\n position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--ink);\n background: rgba(255, 255, 255, 0.92); color: var(--navy); font-size: 22px; cursor: pointer;\n display: flex; align-items: center; justify-content: center; line-height: 1;\n box-shadow: var(--shadow-flat);\n}\n.carousel-nav.prev { left: 10px; } .carousel-nav.next { right: 10px; }\n@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n\n/* bandeau prix + badge marché */\n.f-hero .price { font-size: clamp(34px, 8vw, 44px); }\n.deal-badge {\n display: inline-flex; align-items: center; gap: 6px; margin: 8px 0 4px;\n border-radius: var(--r-pill); padding: 7px 14px; font-size: 13px; font-weight: 600;\n border: 1.5px solid var(--ink);\n}\n.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.deal-ok { background: var(--surface); color: var(--ink-2); }\n.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.f-hero h1 { font-size: clamp(20px, 4.5vw, 26px); margin-top: 8px; }\n\n/* chips clés à défilement horizontal */\n.chips-scroll {\n display: flex; gap: 8px; overflow-x: auto; padding: 12px 0 6px;\n scrollbar-width: none; -webkit-overflow-scrolling: touch;\n}\n.chips-scroll::-webkit-scrollbar { display: none; }\n.chip-key {\n flex: 0 0 auto; background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 8px 15px;\n font-size: 12px; font-weight: 600; white-space: nowrap; min-height: 34px;\n display: inline-flex; align-items: center;\n}\n\n/* ancres de navigation rapide */\n.ancres {\n display: flex; gap: 4px; overflow-x: auto; padding: 6px 0 2px;\n scrollbar-width: none; border-bottom: 1px solid var(--line); margin-bottom: 8px;\n}\n.ancres::-webkit-scrollbar { display: none; }\n.ancres a {\n flex: 0 0 auto; padding: 9px 14px; font-size: 13.5px; font-weight: 600;\n color: var(--ink-2); border-radius: var(--r-pill); min-height: 44px;\n display: inline-flex; align-items: center;\n}\n.ancres a:hover { background: var(--orange-soft); color: var(--orange); }\nhtml { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */\n\n/* description restructurée */\n.enbref {\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-card); padding: 12px 16px; color: var(--navy);\n font-size: 15px; margin: 0 0 14px;\n}\n.desc-section { margin-bottom: 12px; }\n.desc-section h4 { font-size: 15px; margin-bottom: 3px; }\n.desc-section p { color: var(--ink-2); font-size: 14.5px; margin: 0; white-space: pre-line; }\n.texte-original { margin-top: 12px; }\n.texte-original summary {\n cursor: pointer; font-size: 12.5px;\n color: var(--ink-3); min-height: 44px; display: flex; align-items: center;\n}\n.texte-original p { color: var(--ink-3); font-size: 13px; white-space: pre-line; }\n\n/* inclusions : confirmées vs mentionnées */\n.amenity.confirmed { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.25); color: var(--blue); font-weight: 600; }\n.amenity.unconfirmed { background: transparent; border-style: dashed; border-color: var(--line-strong); color: var(--ink-3); }\n\n/* détails pratiques : historique de prix */\n.prix-histo {\n margin-top: 12px; padding: 10px 14px; border-radius: var(--r-card);\n border: 1.5px solid var(--ink); font-size: 13.5px; background: var(--surface);\n}\n.prix-histo.down { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n\n/* quartier compacté sur mobile */\n@media (max-width: 640px) {\n .q-cell { padding: 9px 11px; }\n .q-val { font-size: 16px; }\n .q-bar { gap: 8px; font-size: 12px; }\n .q-bar-track { height: 8px; }\n}\n.q-badge-sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }\n\n/* POI groupés repliables */\n.poi-groupe { border: 1.5px solid var(--ink); border-radius: var(--r-card);\n margin-bottom: 8px; background: var(--surface); overflow: hidden; }\n.poi-groupe summary {\n display: flex; justify-content: space-between; align-items: center; gap: 10px;\n padding: 12px 14px; cursor: pointer; font-weight: 600; font-size: 14px;\n min-height: 48px; list-style: none;\n}\n.poi-groupe summary::-webkit-details-marker { display: none; }\n.poi-groupe summary::after { content: \"▾\"; color: var(--ink-3); transition: transform 0.15s ease; }\n.poi-groupe[open] summary::after { transform: rotate(180deg); }\n.poi-resume { font-size: 11.5px; color: var(--ink-3); font-weight: 400; }\n.poi-groupe .poi-list { padding: 0 14px 10px; }\n\n/* CTA sticky mobile */\n.cta-sticky {\n position: fixed; left: 0; right: 0; z-index: var(--z-dropdown, 700);\n bottom: var(--consent-h, 0px);\n display: flex; align-items: center; gap: 12px;\n background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(12px);\n border-top: 1px solid var(--line);\n padding: 10px 16px calc(10px + env(safe-area-inset-bottom));\n}\n.cta-sticky .cta { flex: 1; margin: 0; text-align: center; }\n.cta-sticky-prix { font-family: var(--font-display); font-weight: 700; font-size: 19px; white-space: nowrap; color: var(--navy); }\n.cta-sticky-prix small { font-weight: 500; font-size: 10px; color: var(--ink-3); }\n@media (min-width: 900px) { .cta-sticky { display: none; } }\n@media (max-width: 899px) {\n .cta-desktop { display: none; }\n .detail { padding-bottom: 84px; } /* place pour le CTA sticky */\n}\n.f-foot { margin-top: 26px; }\n\n/* --- Passerelle vers l'annonce originale ------------------------------------ */\n.passerelle {\n min-height: calc(100dvh - 120px); display: flex; align-items: center;\n justify-content: center; padding: 24px 16px;\n}\n.pass-card {\n width: 100%; max-width: 520px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 30px 28px; text-align: center;\n}\n.pass-brands {\n display: flex; align-items: center; justify-content: center; gap: 14px;\n margin-bottom: 22px; flex-wrap: wrap;\n}\n.pass-brand {\n font-family: var(--font-display); font-weight: 700; font-size: 24px;\n letter-spacing: -0.03em; display: inline-flex; align-items: center; color: var(--navy);\n}\n.pass-brand .ka { color: var(--orange); }\n.pass-trajet { display: inline-flex; align-items: center; gap: 5px; }\n.pass-dot {\n width: 6px; height: 6px; border-radius: 50%; background: var(--orange);\n opacity: 0.25; animation: pass-dot 1.2s ease infinite;\n}\n.pass-dot:nth-child(2) { animation-delay: 0.2s; }\n.pass-dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes pass-dot { 40% { opacity: 1; transform: translateX(2px); } }\n.pass-fleche { color: var(--orange); font-size: 18px; }\n.pass-gestionnaire {\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n display: inline-flex; align-items: center; gap: 8px;\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-pill); padding: 6px 16px; color: var(--blue);\n}\n.pass-card h1 { font-size: 20px; letter-spacing: -0.02em; }\n.pass-texte { color: var(--ink-2); font-size: 14.5px; margin: 10px 0 20px; }\n.pass-progress {\n height: 10px; border-radius: var(--r-pill);\n overflow: hidden; background: var(--grey); margin-bottom: 20px;\n}\n.pass-progress-fill {\n display: block; height: 100%; width: 0; background: var(--orange);\n border-radius: var(--r-pill);\n}\n.pass-progress-fill.go { animation: pass-fill 2.2s linear forwards; }\n@keyframes pass-fill { to { width: 100%; } }\n@media (prefers-reduced-motion: reduce) {\n .pass-progress-fill.go { animation: none; width: 100%; }\n .pass-dot { animation: none; opacity: 0.7; }\n}\n.pass-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; }\n\n/* --- boutons PDF -------------------------------------------------------- */\n.btn-pdf { display: block; text-align: center; margin-top: 10px; width: 100%; }\n\n/* --- Onglets de la page Stats -------------------------------------------- */\n.onglets {\n display: flex; gap: 8px; overflow-x: auto; margin: 26px 0 22px;\n padding-bottom: 4px; scrollbar-width: none;\n border-bottom: 1px solid var(--line);\n}\n.onglets::-webkit-scrollbar { display: none; }\n.onglet {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;\n border: 1.5px solid var(--ink);\n border-radius: var(--r-pill);\n background: var(--surface); color: var(--ink-2); cursor: pointer;\n padding: 10px 18px; font-weight: 600; font-size: 13.5px; min-height: 44px;\n margin-bottom: 8px;\n transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;\n}\n.onglet:hover { background: var(--blue-soft); color: var(--blue); border-color: var(--blue); }\n.onglet.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* baisses de prix */\n.baisses { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }\n.baisses li {\n display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n padding: 9px 4px; border-bottom: 1px solid var(--line); font-size: 13.5px;\n}\n.baisses li a { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }\n.baisse-ville { color: var(--ink-3); font-size: 11.5px; }\n.baisse-prix { margin-left: auto; }\n.baisse-prix s { color: var(--ink-3); }\n.baisse-pct {\n font-style: normal; font-weight: 700;\n background: var(--blue-soft);\n color: var(--blue); border-radius: var(--r-pill); padding: 2px 9px;\n font-size: 11px; margin-left: 8px;\n}\n.alertes { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n\n/* ================= Pagination (12 annonces/page) ================= */\n.pager {\n display: flex; align-items: center; justify-content: space-between;\n gap: 16px; flex-wrap: wrap; margin: 26px 0 10px; padding-top: 18px;\n border-top: 1px solid var(--line);\n}\n.pager-info {\n font-size: 12.5px; color: var(--ink-2); letter-spacing: 0.02em;\n}\n.pager-info em { font-style: normal; color: var(--ink-3); }\n.pager-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.pager-btn {\n min-width: 38px; height: 38px; padding: 0 10px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); cursor: pointer;\n font-size: 13px; font-weight: 600;\n color: var(--navy); transition: all 0.12s ease;\n}\n.pager-btn:hover:not(:disabled):not(.on) {\n background: var(--blue-soft); border-color: var(--blue); color: var(--blue);\n}\n.pager-btn.on { background: var(--navy); border-color: var(--navy); color: var(--white); cursor: default; }\n.pager-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n.pager-ellipsis { color: var(--ink-3); padding: 0 2px; }\n.map-list .pager { margin: 10px 0 4px; }\n@media (max-width: 640px) {\n .pager { justify-content: center; }\n .pager-info { width: 100%; text-align: center; order: 2; }\n}\n\n/* ================= Icônes SVG inline — alignement ================= */\n.chip-ico { display: inline-flex; align-items: center; gap: 6px; }\n.chip-ico svg { flex: none; }\n.view-toggle button, .seg button { display: inline-flex; align-items: center; gap: 6px; }\n.fab { align-items: center; gap: 8px; }\n.fab svg { flex: none; }\n.mm-ico { display: inline-flex; align-items: center; }\n.notice .big { display: flex; justify-content: center; color: var(--ink-3); }\n.notice .big svg { display: block; }\n.card-img .noimg { color: var(--ink-3); }\n.badge.right { display: inline-flex; align-items: center; gap: 4px; }\n.btn-pdf { display: inline-flex; align-items: center; gap: 7px; }\n.f-search svg { flex: none; color: var(--ink-3); }\n.mv-noimg { display: flex; align-items: center; justify-content: center; color: var(--ink-3); }\n\n/* footer — liens (Groupe KA, courriels, mentions légales) */\n.footer .frow a { color: var(--white); text-decoration: underline; text-decoration-color: rgba(255, 106, 0, 0.5); text-underline-offset: 3px; }\n.footer .frow a:hover { color: var(--orange); }\n.footer .fmono a:hover { color: var(--orange); }\n\n/* ================= Compte (connexion Google) ================= */\n.login-btn {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 8px 16px; margin-left: 10px; min-height: 38px;\n background: var(--navy); border: none;\n border-radius: var(--r-pill); font-weight: 600; font-size: 13.5px; color: var(--white);\n transition: all 0.12s ease; white-space: nowrap;\n box-shadow: 0 4px 14px rgba(20, 24, 20, 0.22);\n}\n.login-btn:hover { background: var(--blue); }\n.account { position: relative; margin-left: 10px; }\n.account-btn {\n width: 38px; height: 38px; padding: 0; border: 2px solid var(--orange);\n border-radius: var(--r-pill); overflow: hidden; cursor: pointer;\n background: var(--orange-soft); display: flex; align-items: center; justify-content: center;\n}\n.account-btn img { width: 100%; height: 100%; object-fit: cover; }\n.account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }\n/* Click-catcher plein écran rendu via createPortal à la racine de \n (un fixed descendant du header blurré se re-parentait → 66 px de haut).\n z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le\n header et doit peindre au-dessus du voile. */\n.account-backdrop { position: fixed; inset: 0; z-index: 40; }\n.account-menu {\n position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;\n min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off); overflow: hidden;\n}\n.account-id { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-bottom: 1px solid var(--line); }\n.account-id b { font-size: 14px; }\n.account-id span { font-size: 11.5px; color: var(--ink-3); word-break: break-all; }\n.account-menu button {\n width: 100%; text-align: left; padding: 11px 14px; border: 0; cursor: pointer;\n background: transparent; font-weight: 600; font-size: 13.5px; color: var(--danger);\n}\n.account-menu button:hover { background: var(--orange-soft); color: var(--navy); }\n@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n\n/* ================= Profil — carte de membre Groupe KA ================= */\n.profil { padding-top: 40px; padding-bottom: 60px; }\n.profil h1 { font-size: clamp(30px, 5vw, 44px); margin: 10px 0 26px; color: var(--navy); }\n.profil .lede { max-width: 520px; color: var(--ink-2); margin-bottom: 22px; }\n\n.pc {\n position: relative; max-width: 520px; overflow: hidden;\n background: linear-gradient(135deg, #101a40 0%, var(--navy) 55%, #080e24 100%);\n color: var(--white);\n border-radius: 20px;\n padding: 24px 26px 0; box-shadow: var(--shadow-off);\n}\n.pc-watermark {\n position: absolute; right: -18px; top: -34px; pointer-events: none;\n font-family: var(--font-display); font-weight: 700; font-size: 170px;\n letter-spacing: -0.06em; color: rgba(255, 106, 0, 0.07);\n transform: rotate(-8deg); line-height: 1;\n}\n.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }\n.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }\n.pc-ka { color: var(--orange); margin-left: 1px; display: inline-block; }\n.pc-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.5); }\n.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }\n.pc-id-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2em; color: rgba(255, 106, 0, 0.75); }\n.pc-id {\n font-weight: 700; color: var(--orange);\n font-size: clamp(22px, 6vw, 34px); letter-spacing: 0.14em;\n text-shadow: 0 0 24px rgba(255, 106, 0, 0.35);\n}\n.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }\n.pc-holder { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pc-holder-name { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pc-holder-since { font-size: 10.5px; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.06em; }\n.pc-role-badge {\n display: inline-block; width: fit-content; margin: 3px 0 2px;\n background: var(--orange); color: var(--white);\n font-size: 9px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.12em;\n padding: 2px 8px 3px; border-radius: var(--r-pill);\n}\n.pc-avatar { width: 52px; height: 52px; border-radius: var(--r-pill); border: 2px solid var(--orange); flex: none; }\n.pc-strip { display: flex; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(255, 106, 0, 0.1); border-top: 1px solid rgba(255, 106, 0, 0.25); overflow: hidden; }\n.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--orange); opacity: 0.7; }\n.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.35; }\n.pc-strip i:nth-child(3n+1) { height: 9px; }\n.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.9; }\n.pc-copy { margin-top: 16px; }\n.pc-copy.ok { background: var(--blue); color: var(--white); }\n\n.profil-grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 12px; margin-top: 30px; max-width: 760px;\n}\n.pg-item {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 14px 16px;\n display: flex; flex-direction: column; gap: 4px; box-shadow: var(--shadow-flat);\n}\n.pg-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }\n.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }\n.pg-value.mono { color: var(--blue); }\n\n.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }\n.profil-note a { text-decoration: underline; text-underline-offset: 3px; }\n.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }\n.profil-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n\n.account-kaid { font-size: 10.5px; color: var(--blue); letter-spacing: 0.08em; }\n.account-link {\n display: block; width: 100%; text-align: left; padding: 11px 14px;\n font-weight: 600; font-size: 13.5px; color: var(--navy);\n border-bottom: 1px solid var(--line);\n}\n.account-link:hover { background: var(--orange-soft); }\n\n/* profil public — section + lien partageable */\n.profil-public { margin-top: 30px; max-width: 640px; }\n.profil-public h3 { font-size: 18px; margin-bottom: 6px; }\n.profil-public p { color: var(--ink-2); font-size: 13.5px; margin: 0 0 12px; }\n.public-link {\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n margin-top: 14px; padding: 12px 14px; background: var(--blue-soft);\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-card);\n}\n.public-link a { font-size: 13px; color: var(--blue); text-decoration: underline; text-underline-offset: 3px; word-break: break-all; }\n\n/* ================= Bienvenue — choix du rôle ================= */\n.bienvenue { padding-top: 44px; padding-bottom: 70px; }\n.bienvenue h1 { font-size: clamp(30px, 5vw, 46px); margin: 10px 0 14px; color: var(--navy); }\n.bienvenue .lede { max-width: 480px; color: var(--ink-2); margin-bottom: 30px; }\n.role-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; max-width: 780px; }\n.role-card {\n display: flex; flex-direction: column; align-items: flex-start; gap: 10px;\n text-align: left; padding: 24px 24px 20px; cursor: pointer;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-flat);\n font-family: var(--font-body); transition: all 0.15s ease;\n}\n.role-card:hover:not(:disabled) { transform: translateY(-4px); box-shadow: var(--shadow-off); border-color: rgba(255, 106, 0, 0.4); }\n.role-card:disabled { opacity: 0.6; cursor: wait; }\n.role-ico { display: flex; width: 54px; height: 54px; align-items: center; justify-content: center; background: var(--orange-soft); color: var(--orange); border-radius: 16px; }\n.role-title { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.role-desc { color: var(--ink-2); font-size: 13.5px; }\n.role-perks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }\n.role-perks li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }\n.role-perks li::before {\n content: \"✓\"; flex: none; width: 16px; height: 16px; border-radius: 50%;\n background: var(--orange); color: var(--white); font-size: 10px; font-weight: 700;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.role-cta { margin-top: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--orange); }\n\n/* ================= Favoris — cœur sur les cartes ================= */\n.fav-btn {\n position: absolute; right: 10px; bottom: 10px; z-index: 2;\n width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;\n background: rgba(255, 255, 255, 0.94); border: none;\n border-radius: var(--r-pill); cursor: pointer; color: var(--navy);\n box-shadow: var(--shadow-flat);\n transition: transform 0.12s ease, background 0.12s ease, color 0.12s ease;\n}\n.fav-btn:hover { transform: scale(1.12); }\n.fav-btn.on { background: var(--white); color: var(--orange); }\n.fav-btn.on svg { filter: drop-shadow(0 0 3px rgba(255, 106, 0, 0.35)); }\n\n/* ================= Espace gestionnaire ================= */\n.claim-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; max-width: 640px; }\n.claim-row select {\n flex: 1; min-width: 260px; padding: 11px 14px; font-size: 14px;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); font-family: var(--font-body);\n}\n.claim-error { color: var(--danger); font-size: 13.5px; font-weight: 600; margin-top: 10px; }\n.org-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 22px; }\n.org-stat { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); }\n.org-form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }\n.org-form h3 { font-size: 18px; }\n.org-form label { display: flex; flex-direction: column; gap: 5px; flex: 1; }\n.org-form label span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }\n.org-form input, .org-form textarea {\n padding: 10px 14px; font-size: 14px; font-family: var(--font-body);\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);\n}\n.org-form textarea { resize: vertical; }\n.org-form input:focus, .org-form textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.org-form-row { display: flex; gap: 12px; flex-wrap: wrap; }\n.org-form .btn { align-self: flex-start; }\n\n/* ================= Page publique /g/{source} ================= */\n.orgpub-head { display: flex; gap: 20px; align-items: flex-start; margin-top: 8px; }\n.orgpub-logo { width: 84px; height: 84px; object-fit: contain; background: var(--surface); border: 1.5px solid var(--ink); border-radius: 18px; padding: 8px; flex: none; box-shadow: var(--shadow-flat); }\n.orgpub-id h1 { font-size: clamp(26px, 4.4vw, 40px); margin: 6px 0 8px; color: var(--navy); }\n.orgpub-tagline { font-size: 16px; color: var(--ink-2); margin: 0 0 12px; max-width: 640px; }\n.orgpub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.orgpub-meta a.stat-chip:hover { background: var(--blue-soft); }\n.orgpub-verified { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); font-weight: 600; }\n.orgpub-desc { max-width: 720px; color: var(--ink-2); margin: 18px 0 6px; white-space: pre-line; }\n.orgpub .results-head h2 { display: inline-flex; align-items: center; gap: 8px; }\n@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n\n/* ================= Éditeur de profil (photo, bio, réseaux) ================= */\n.profil-edit { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--line-strong); }\n/* profil géré au hub Groupe KA — lecture seule */\n.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }\n.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }\n.hub-profile a.stat-chip:hover { background: var(--blue-soft); }\n.hub-profile .btn-primary { display: inline-flex; align-items: center; gap: 7px; }\n.pe-avatar-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.pe-avatar {\n width: 84px; height: 84px; border-radius: var(--r-pill); object-fit: cover;\n border: 2.5px solid var(--orange); box-shadow: var(--shadow-flat);\n display: flex; align-items: center; justify-content: center;\n}\n.pe-avatar-empty { background: var(--orange-soft); color: var(--orange); font-family: var(--font-display); font-weight: 700; font-size: 34px; }\n.pe-avatar-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }\n.pe-avatar-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n.pe-hint { font-size: 11px; color: var(--ink-3); }\n.pe-socials-title { font-size: 15px; margin-top: 6px; }\n.pe-socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }\n.pe-social { display: flex !important; flex-direction: row !important; align-items: center; gap: 0 !important; }\n.pe-social-ico {\n display: flex; align-items: center; justify-content: center;\n width: 40px; align-self: stretch; flex: none;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-ctl) 0 0 var(--r-ctl);\n}\n.pe-social input { flex: 1; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; border-left: 0; }\n\n/* profil public — à propos + réseaux */\n.pub-about { max-width: 640px; margin-top: 22px; }\n.pub-bio { font-size: 15.5px; color: var(--ink); margin: 0 0 12px; white-space: pre-line; }\n.pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.pub-socials { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }\n.pub-social {\n display: flex; align-items: center; justify-content: center;\n width: 44px; height: 44px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: 14px; color: var(--navy);\n transition: all 0.12s ease;\n}\n.pub-social:hover { background: var(--navy); border-color: var(--navy); color: var(--white); transform: translateY(-2px); box-shadow: var(--shadow-off-soft); }\n\n/* -----------------------------------------------------------------------------\n Lou-Ka Maps (framework Ka Maps par Groupe Ka)\n Jetons du design system appliqués au chrome de la carte + sync liste↔carte.\n----------------------------------------------------------------------------- */\n.mapview .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n}\n.mapview { position: relative; overflow: hidden; }\n.mapview .ka-map { position: absolute; inset: 0; }\n\n/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en orange */\n.lmap3d {\n position: relative; height: 340px; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.lmap3d .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n position: absolute; inset: 0;\n}\n.lmap3d-skel {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.lmap3d-legende {\n position: absolute; left: 10px; bottom: 10px; z-index: 5;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 4px 10px; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n font-size: 10.5px; font-weight: 500; color: var(--ink-2);\n pointer-events: none;\n}\n.lmap3d-legende i {\n width: 10px; height: 10px; border-radius: 3px;\n background: var(--orange); border: 1px solid rgba(20, 24, 20, 0.25);\n}\n@media (max-width: 780px) { .lmap3d { height: 280px; } }\n.ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }\n.ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }\n.ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }\n\n/* carte de liste synchronisée avec la carte (survol / sélection) */\n.map-card { border-radius: var(--r-card); }\n.map-card-sel { outline: 2px solid var(--blue); outline-offset: 2px; }\n\n/* =============================================================================\n HARMONISATION « ÉDITORIAL SHARP » (Groupe KA) — couche finale posée après\n les styles historiques : géométrie 10/6 px, bordures encre 1,5 px, ombres\n décalées dures, accent orange Lou-Ka. La carte Ka Maps (Mapbox) conserve\n son propre thème marine/orange à l'intérieur du canevas.\n============================================================================= */\n\n/* — contrôles : plus de pilules (les chips/badges gardent --r-pill) — */\n.nav a, .login-btn, .cta, .mv-pop-cta, .f-search, .f-more, .pager-btn,\n.onglet, .view-toggle, .fab, .f-ctl, .pass-gestionnaire, .hbar-track,\n.q-bar-track, .pass-progress { border-radius: var(--r-ctl); }\n.seg button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); }\n.seg button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }\n\n/* — header : papier translucide + trait d'encre — */\n.header {\n background: rgba(245, 243, 238, 0.92);\n border-bottom: 1.5px solid var(--ink);\n}\n.nav a { font-family: var(--font-display); }\n.nav a:hover { color: var(--accent-deep); background: var(--accent-soft); }\n.nav a.active { background: var(--ink); color: var(--accent); }\n.brand .ka {\n background: var(--accent); color: var(--on-accent);\n border-radius: 6px; padding: 0 6px 2px; margin-left: 2px; transform: rotate(-2deg);\n}\n.header .gk-badge { margin-left: 4px; flex: none; }\n@media (max-width: 1080px) { .header .gk-badge { display: none; } }\n.mm-badge {\n display: flex; align-items: center; min-height: var(--touch);\n padding: 8px 10px 2px;\n}\n\n/* — ticker : encre + mono — */\n.ticker { font-family: var(--font-mono); border-bottom: 1.5px solid var(--ink); }\n\n/* — états sélectionnés : encre + accent (fini le bleu) — */\n.seg button.on, .onglet.on, .view-toggle button.on, .f-more.on {\n background: var(--ink); border-color: var(--ink); color: var(--accent);\n}\n.pager-btn.on { background: var(--ink); border-color: var(--ink); color: var(--accent); }\n\n/* — CTA / FAB : aplat accent, bordure encre, ombre décalée — */\n.cta, .mv-pop-cta, .fab {\n background: var(--accent); color: var(--on-accent);\n border: 1.5px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);\n font-family: var(--font-display); font-weight: 700;\n}\n.cta:hover, .mv-pop-cta:hover, .fab:hover { background: var(--accent-deep); }\n.cta:active, .fab:active { box-shadow: none; }\n.fab { box-shadow: 4px 4px 0 var(--ink), 0 10px 28px rgba(20, 24, 20, 0.25); }\n.login-btn {\n background: var(--ink); color: var(--accent);\n border: 1.5px solid var(--ink); box-shadow: none;\n font-family: var(--font-display); font-weight: 700;\n}\n.login-btn:hover { background: var(--accent-deep); color: var(--on-accent); }\n\n/* — cartes : levée nette diagonale (signature du groupe) — */\n.card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }\n.role-card:hover:not(:disabled) { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); border-color: var(--ink); }\n.badge { border: 1.5px solid var(--ink); border-radius: var(--r-ctl); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.badge.type { background: var(--accent); color: var(--on-accent); }\n.source-tag { font-family: var(--font-mono); }\n\n/* — barre d'onglets mobile : papier + encre, actif = accent orange — */\n@media (max-width: 760px) {\n .tabbar { background: rgba(245, 243, 238, 0.95); border-top: 1.5px solid var(--ink); }\n .tabbar a { font-family: var(--font-display); }\n .tabbar a.active { color: var(--accent); }\n}\n.mobile-menu { background: var(--paper); border-bottom: 1.5px solid var(--ink); }\n\n/* — carte de membre KA : encre plutôt que marine — */\n.pc { background: linear-gradient(135deg, #1d231d 0%, var(--ink) 55%, #0b0e0b 100%); }\n\n/* — micro-étiquettes en mono (système) — */\n.f-ctl > span, .f-group > label, .field label, .tile-k, .pg-label,\n.pc-label, .pc-id-label, .crumbs, .viz-sub { font-family: var(--font-mono); }\n\n/* — page Contact (design system) — */\n.contact-page { padding: 44px 0 90px; }\n.contact-page h1 { margin: 10px 0 8px; }\n.contact-page .lede { color: var(--ink-2); max-width: 640px; margin: 0 0 26px; }\n.contact-grid { display: grid; gap: 16px; grid-template-columns: 1fr; margin: 26px 0; }\n@media (min-width: 768px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }\n.contact-card { padding: 18px 20px; }\n.contact-card .klabel { display: block; margin-bottom: 6px; }\n.contact-card a.mail {\n font-family: var(--font-mono); font-weight: 700; font-size: 13.5px;\n color: var(--accent-deep); word-break: break-all;\n text-decoration: underline; text-underline-offset: 3px;\n display: inline-flex; align-items: center; min-height: 44px;\n}\n.contact-card p { margin: 6px 0 0; color: var(--ink-2); font-size: 13.5px; }\n.contact-note {\n border-left: 3px solid var(--accent); background: var(--surface);\n border-top: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);\n border-bottom: 1.5px solid var(--ink); border-radius: 0 var(--r-card) var(--r-card) 0;\n padding: 14px 18px; max-width: 720px; color: var(--ink-2); font-size: 14px;\n}\n.contact-note b { color: var(--ink); }\n.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.contact-sites a {\n display: inline-flex; align-items: center; min-height: 34px;\n padding: 4px 12px; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); font-family: var(--font-mono); font-size: 11px;\n font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;\n transition: transform 0.12s, box-shadow 0.12s;\n}\n.contact-sites a:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.contact-hub { margin-top: 26px; }\n\n/* — rangée de navigation locale au-dessus du footer commun — */\n.prefooter { border-top: 1.5px solid var(--ink); background: var(--surface-2); padding: 18px 0; }\n.prefooter .container {\n display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px;\n font-family: var(--font-mono); font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n}\n.prefooter a, .prefooter .flink {\n color: var(--ink-2); text-decoration: none; text-transform: uppercase;\n letter-spacing: 0.08em; font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;\n min-height: var(--touch); display: inline-flex; align-items: center;\n}\n.prefooter a:hover, .prefooter .flink:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }\n.prefooter .baseline { width: 100%; font-family: var(--font-body); font-weight: 400; font-size: 13px; color: var(--ink-2); text-transform: none; letter-spacing: 0; }\n.ka-footer { margin-top: 0; }\n@media (max-width: 760px) { .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } }\n\n/* ============ Juste valeur (fair value) — pastilles & analyse de prix ====== */\n.fv-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border-radius: var(--r-pill); padding: 6px 12px;\n font-size: 12.5px; font-weight: 700; border: 1.5px solid var(--ink);\n white-space: nowrap;\n}\n.fv-badge .fv-ico { font-size: 10px; line-height: 1; }\n.fv-badge .fv-pct { margin-left: 4px; }\n.fv-badge.fv-compact { padding: 2px 8px; font-size: 11px; gap: 4px; margin-left: 8px; vertical-align: 2px; }\n/* états (mêmes jetons que les badges marché existants — daltonien-compatible :\n l'icône ▼ ≈ ▲ et le libellé portent l'information, pas la couleur seule) */\n.fv-badge.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.35); color: var(--blue); }\n.fv-badge.deal-ok { background: var(--surface); color: var(--ink-2); }\n.fv-badge.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.4); color: var(--orange-deep); }\n\n.f-fairvalue .fv-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 12px; flex-wrap: wrap; margin-bottom: 6px;\n}\n.f-fairvalue .fv-value { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--navy); }\n.f-fairvalue .fv-value small { font-size: 12px; font-weight: 500; color: var(--ink-3); }\n.f-fairvalue .fv-range { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.fv-histo { display: block; width: 100%; height: auto; margin: 10px 0 4px; }\n.fv-histo-lbl { font: 700 10px Inter, system-ui, sans-serif; fill: var(--ink); }\n.fv-histo-lbl-fv { fill: var(--accent-deep, #cc5500); }\n.fv-histo-axis { font: 500 9px Inter, system-ui, sans-serif; fill: var(--ink-3); }\n.fv-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12.5px; color: var(--ink-2); margin: 6px 0 4px; }\n.fv-demo { display: flex; flex-wrap: wrap; gap: 8px; }\n.chip.chip-deal.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* =============================================================================\n MapSearch — recherche synchronisée liste ↔ carte (search/MapSearch.tsx)\n Une seule vérité partagée : compteur, tri, survols et sélections miroirs.\n============================================================================= */\n\n.ms { position: relative; }\n.ms-list { position: relative; scroll-behavior: smooth; }\n\n.ms-list-head {\n display: flex; align-items: center; justify-content: space-between;\n gap: 10px; flex-wrap: wrap; padding: 2px 2px 4px;\n}\n.ms-count { font-size: 13.5px; color: var(--ink-2); }\n.ms-count b { color: var(--ink); font-size: 15px; }\n.ms-nopos {\n margin-left: 8px; font-size: 11px; color: var(--ink-3);\n border: 1px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 8px; cursor: help;\n}\n.ms-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }\n.ms-sort select {\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); padding: 6px 8px; font-size: 12.5px;\n font-weight: 600; color: var(--ink);\n}\n\n.ms-pills { margin: 0 0 4px; }\n\n.ms-refit {\n align-self: flex-start; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--ink-2);\n cursor: pointer; box-shadow: var(--shadow-off-soft);\n}\n.ms-refit:hover { background: var(--navy); color: var(--white); border-color: var(--navy); }\n\n/* barre de progression fine — jamais d'écran qui clignote */\n.ms-progress {\n position: sticky; top: 0; z-index: 4; height: 3px; border-radius: 2px;\n background: linear-gradient(90deg, transparent, var(--accent), transparent);\n background-size: 200% 100%; animation: ms-progress 1s linear infinite;\n}\n@keyframes ms-progress { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n.ms-empty {\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-card);\n padding: 26px 18px; text-align: center; color: var(--ink-2);\n}\n.ms-empty .big { margin-bottom: 6px; color: var(--ink-3); }\n.ms-empty h3 { margin: 0 0 6px; font-size: 16px; color: var(--ink); }\n.ms-empty p { margin: 0 0 14px; font-size: 13px; }\n.ms-empty-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n/* miroirs survol / sélection venus de la carte */\n.map-card { transition: transform 0.15s ease, box-shadow 0.15s ease; }\n.map-card-hover {\n outline: 2px solid var(--accent); outline-offset: 2px;\n transform: translateY(-1px);\n}\n.map-card-pulse { animation: ms-pulse 0.9s ease 1; }\n@keyframes ms-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* indicateur discret : l'annonce survolée sur la carte est hors écran */\n.ms-hover-hint {\n position: sticky; z-index: 5; align-self: center;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 5px 12px;\n font-size: 11.5px; font-weight: 600; pointer-events: none;\n box-shadow: var(--shadow-off-soft);\n}\n.ms-hover-hint.haut { top: 8px; order: -1; }\n.ms-hover-hint.bas { bottom: 8px; }\n\n/* ---------------- carrousel mobile synchronisé aux marqueurs ------------- */\n.ms-carousel {\n position: absolute; left: 0; right: 0; bottom: 10px; z-index: 12;\n display: flex; gap: 10px; overflow-x: auto; padding: 4px 12px;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n.ms-carousel::-webkit-scrollbar { display: none; }\n.ms-car-item {\n flex: 0 0 78%; max-width: 340px; scroll-snap-align: center;\n display: grid; grid-template-columns: 108px 1fr; gap: 0;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); overflow: hidden;\n box-shadow: var(--shadow-off-soft); text-decoration: none; color: inherit;\n}\n.ms-car-item.sel { outline: 2.5px solid var(--accent); outline-offset: 1px; }\n.ms-car-item img, .ms-car-item .smart-img, .ms-car-item .card-fallback {\n width: 108px; height: 96px; object-fit: cover; display: block;\n}\n.ms-car-body { padding: 9px 12px; min-width: 0; }\n.ms-car-price { font-size: 14.5px; font-weight: 700; color: var(--ink); display: flex; gap: 6px; align-items: center; }\n.ms-car-title {\n font-size: 12px; color: var(--ink-2); margin-top: 2px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.ms-car-meta { font-size: 11px; color: var(--ink-3); margin-top: 3px; }\n.ms-car-more {\n flex: 0 0 auto; scroll-snap-align: center; align-self: stretch;\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n background: var(--navy); color: var(--white);\n font-size: 12.5px; font-weight: 600; padding: 0 18px; cursor: pointer;\n}\n\n/* mobile : carte plein cadre, liste masquée, carrousel par-dessus la carte */\n@media (max-width: 780px) {\n .ms { height: calc(100dvh - 190px); }\n .ms-list { display: none; }\n .ms .mapview { height: 100%; }\n .ka-search-area { top: 8px; }\n}\n@media (min-width: 781px) {\n .ms-carousel { display: none; }\n}\n\n/* mobile : réorganisation du chrome carte autour du carrousel */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 56px; left: 10px; bottom: auto; }\n .ms .ka-loading { bottom: 132px; }\n .ms .ka-empty { transform: translate(-50%, -70%); }\n}\n/* l'auto-défilement vers la carte laisse l'entête de résultats visible */\n.ms { scroll-margin-top: 108px; }\n\n/* mobile, vue carte : chrome sans chevauchement (case en haut, outils sous\n elle, FAB Filtres au-dessus du carrousel) */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 88px; left: 10px; bottom: auto; }\n .ms .ka-locate { top: 88px; right: 10px; }\n .fab-carte { bottom: 148px; }\n}\n\n/* =============================================================================\n KA Scores — pastilles, jauges circulaires, page méthodologie\n============================================================================= */\n.ka-badge {\n display: inline-flex; align-items: center; gap: 4px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 1px 8px 1px 3px; font-size: 11px; font-weight: 700;\n color: var(--ink); background: var(--surface); line-height: 1.5;\n vertical-align: middle;\n}\n.ka-badge-logo {\n background: var(--ink); color: var(--white); border-radius: 999px;\n font-size: 8.5px; font-weight: 800; padding: 1.5px 5px; letter-spacing: 0.4px;\n}\n.ka-badge.ka-haut { border-color: var(--green, #256d43); }\n.ka-badge.ka-haut .ka-badge-logo { background: var(--green, #256d43); }\n.ka-badge.ka-bon { border-color: var(--accent); }\n.ka-badge.ka-bon .ka-badge-logo { background: var(--accent); }\n.ka-badge.ka-moyen { opacity: 0.9; }\n.ka-badge.ka-bas { opacity: 0.75; }\n\n.ka-circles, .ka-demo-circles {\n display: flex; gap: 18px; flex-wrap: wrap; margin: 14px 0 6px;\n}\n.ka-circle { text-align: center; width: 92px; }\n.ka-circle-fond {\n fill: none; stroke: var(--line); stroke-width: 6;\n}\n.ka-circle-arc {\n fill: none; stroke: var(--ink-3); stroke-width: 6; stroke-linecap: round;\n transition: stroke-dasharray 0.6s ease;\n}\n.ka-circle.ka-haut .ka-circle-arc { stroke: var(--green, #256d43); }\n.ka-circle.ka-bon .ka-circle-arc { stroke: var(--accent); }\n.ka-circle.ka-moyen .ka-circle-arc { stroke: var(--orange, #d97a2b); }\n.ka-circle.ka-bas .ka-circle-arc { stroke: #b3543f; }\n.ka-circle-val { font: 700 17px var(--font-body); fill: var(--ink); }\n.ka-circle-nom { font-size: 12px; font-weight: 700; color: var(--ink); margin-top: 4px; }\n.ka-circle-label { font-size: 10.5px; color: var(--ink-3); }\n\n.f-kascores h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ka-perso {\n font-size: 12px; font-weight: 600; color: var(--ink-2);\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 10px;\n}\n.ka-detail summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n padding: 6px 0;\n}\n.ka-detail-grille {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px; padding: 8px 0 4px;\n}\n.ka-detail-grille h4 { margin: 0 0 4px; font-size: 12px; color: var(--ink); }\n.ka-detail-grille ul { margin: 0 0 10px; padding-left: 16px; font-size: 12.5px; color: var(--ink-2); }\n.ka-detail-grille li { margin: 2px 0; }\n\n.ka-poids { display: grid; gap: 10px; margin: 14px 0; max-width: 460px; }\n.ka-poids-row {\n display: grid; grid-template-columns: 90px 1fr 48px; align-items: center;\n gap: 12px; font-size: 13px; font-weight: 600; color: var(--ink);\n}\n.ka-poids-row input[type=\"range\"] { accent-color: var(--accent); }\n.ka-poids-row b { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ms-car-price .ka-badge { margin-left: 2px; }\n@media (max-width: 780px) {\n .ms .ka-3d { top: 148px; right: 10px; }\n}\n\n\n/* ---- Registre des loyers (fiche) — bloc citoyen registre-des-loyers.ca ---- */\n.f-rdl .rdl-resume { margin: 2px 0 6px; }\n.f-rdl .rdl-ecart { margin: 0 0 8px; }\n.rdl-rooms { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 10px; }\n.rdl-room { font-size: 13px; color: var(--ink-2); white-space: nowrap; }\n.rdl-room i { font-style: normal; color: var(--ink-3); }\n.rdl-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.rdl-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line, #e6e4df);\n vertical-align: top; }\n.rdl-table .rdl-addr { max-width: 46%; }\n.rdl-table .rdl-prix { font-weight: 600; white-space: nowrap; }\n.rdl-table .rdl-date { white-space: nowrap; color: var(--ink-2); }\n.rdl-table .rdl-dist { color: var(--ink-3); white-space: nowrap; text-align: right; }\n.rdl-room-on { background: var(--sand, #f4efe7); border-radius: 6px; padding: 2px 8px; }\n\n\n/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */\n.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }\n.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n font-size: 12.5px; font-weight: 700; }\n.zi-eleve { background: #fde8e8; color: #a12622; }\n.zi-modere { background: #fdf3e0; color: #8a5a00; }\n.zi-present { background: #fdf3e0; color: #8a5a00; }\n.zi-ok { background: #e7f4ea; color: #1e6b34; }\n.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n\n\n/* ---- Historique TAL (fiche) — décisions SOQUIJ à l'adresse ---- */\n.f-tal .tal-pending, .f-tal .tal-ok, .f-tal .tal-resume { margin: 2px 0 8px; }\n.tal-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.tal-liste li { padding: 8px 0; border-top: 1px solid var(--line, #e6e4df); }\n.tal-ligne { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }\n.tal-date { font-weight: 600; color: var(--ink-2); }\n.tal-part { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: var(--sand, #f0ede8); color: var(--ink-2); }\n.tal-part-locateur { background: #fdf3e0; color: #8a5a00; }\n.tal-verdict { font-size: 12px; color: var(--ink-3); }\n.tal-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 5px 0 3px; }\n.tal-tag { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: #fde8e8; color: #a12622; }\n.tal-ref { font-size: 12.5px; color: var(--ink-3); }\n\n\n/* ---- Registre des loyers v2 : tuiles, bande de position, barres ---- */\n.rdl-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 12px; }\n.rdl-kpi { flex: 1 1 90px; min-width: 90px; background: var(--surface-2, #f7f5f1);\n border-radius: 10px; padding: 10px 12px; }\n.rdl-kpi-v { display: block; font-size: 20px; font-weight: 700;\n letter-spacing: -0.02em; color: var(--navy); }\n.rdl-kpi-l { display: block; font-size: 11px; line-height: 1.35;\n color: var(--ink-3); margin-top: 2px; }\n.rdl-strip { width: 100%; height: auto; display: block; margin: 2px 0 4px; }\n.rdl-strip-track { fill: var(--surface-2, #f0ede8); }\n.rdl-strip-box { fill: var(--accent-soft, #fff1e6);\n stroke: var(--accent, #ff6a00); stroke-width: 1; }\n.rdl-strip-med { stroke: var(--ink-2, #4c4a45); stroke-width: 2; }\n.rdl-strip-me { stroke: var(--accent-deep, #cc5500); stroke-width: 2; }\n.rdl-strip-me-dot { fill: var(--accent-deep, #cc5500);\n stroke: var(--surface, #fff); stroke-width: 2; }\n.rdl-strip-lbl, .rdl-strip-axis { font-size: 10.5px; fill: var(--ink-3); }\n.rdl-strip-me-lbl { font-size: 11px; font-weight: 700;\n fill: var(--accent-deep, #cc5500); }\n.rdl-bars { width: 100%; height: auto; display: block; margin: 6px 0 2px; }\n.rdl-bar { fill: var(--accent, #ff6a00); opacity: 0.55; }\n.rdl-bar-on { fill: var(--accent-deep, #cc5500); opacity: 1; }\n.rdl-bar-val { font-size: 10.5px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat { font-size: 11px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat-on { font-weight: 700; fill: var(--ink, #1c1b18); }\n.rdl-bar-n { font-size: 9.5px; fill: var(--ink-3, #8a877f); }\n.rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }\n.rdl-ecart { margin: 4px 0 8px; }\n.rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }\n\n\n/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */\n.q-crime { width: 100%; }\n.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }\n.q-crime-cats li { display: flex; align-items: center; gap: 8px;\n padding: 3px 0; font-size: 12.5px; }\n.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.q-crime-barre i { display: block; height: 100%; border-radius: 5px;\n background: var(--accent, #ff6a00); opacity: 0.6; }\n.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; }\n.q-crime-n small { font-weight: 400; color: var(--ink-3); }\n.q-crime-src { margin-top: 8px; }\n\n\n/* ---- Qualité de l'air (RSQAQ) + Essence à proximité ---- */\n.air-liste { list-style: none; margin: 10px 0 0; padding: 0; }\n.air-liste li { display: flex; align-items: center; gap: 8px;\n padding: 4px 0; font-size: 12.5px; }\n.air-nom { flex: 0 0 44%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.air-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.air-barre i { display: block; height: 100%; border-radius: 5px;\n background: #4d9e64; }\n.air-barre i.air-sur { background: #c96a1f; }\n.air-val { flex: 0 0 34%; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.air-val small { font-weight: 400; color: var(--ink-3); }\n.air-val .air-ref { display: block; font-size: 10px; }\n.gaz-head th { text-align: left; font-size: 11px; color: var(--ink-3);\n font-weight: 600; padding: 2px 8px 2px 0; }\n.gaz-best { display: inline-block; margin-left: 6px; padding: 1px 7px;\n border-radius: 999px; background: #e7f4ea; color: #1e6b34;\n font-size: 10.5px; font-weight: 700; }\n.gaz-adr { display: block; font-size: 11px; color: var(--ink-3); }\n\n\n/* ---- Commerces et transport (fiche) ---- */\n.cm-grille { list-style: none; margin: 6px 0 0; padding: 0;\n display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }\n@media (max-width: 560px) { .cm-grille { grid-template-columns: 1fr; } }\n.cm-item { display: flex; align-items: center; gap: 9px; padding: 5px 0;\n border-bottom: 1px solid var(--line, #eeece7); min-width: 0; }\n.cm-ico { flex: 0 0 28px; }\n.cm-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.cm-nom { font-size: 13px; font-weight: 600; color: var(--ink, #222);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-poi { font-size: 11px; color: var(--ink-3, #8a877f);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-dist { flex: 0 0 auto; font-size: 13px; font-weight: 700;\n color: var(--ink-2, #4c4a45); white-space: nowrap; }\n\n\n/* ---- Coût d'électricité (Hydro-Québec) ---- */\n.hydro-intro { margin: 2px 0 12px; color: var(--ink-2, #4c4a45); }\n.hydro-btn { appearance: none; border: 0; border-radius: 10px; cursor: pointer;\n background: var(--accent, #2b7a3b); color: #fff; font-weight: 700;\n font-size: 14px; padding: 11px 18px; }\n.hydro-btn:disabled { opacity: 0.6; cursor: progress; }\n.hydro-montant { font-size: 26px; font-weight: 800; letter-spacing: -0.02em;\n color: var(--navy, #1c1b18); display: flex; align-items: baseline;\n gap: 10px; flex-wrap: wrap; }\n.hydro-montant small { font-size: 14px; font-weight: 500; color: var(--ink-3); }\n.hydro-an { font-size: 14px; font-weight: 600; color: var(--ink-2, #4c4a45); }\n.hydro-vide { color: var(--ink-3, #8a877f); }\n\n\n/* ============================================================================\n PASSE DE DESIGN v3 — « éditorial sharp » appliqué à toute la fiche\n Blocs = cartes encre (bordure 1,5 px + ombre décalée douce), titres à\n marqueur accent + filet, tuiles KPI blanches à ombre dure, tableaux zébrés,\n barres en dégradé, pastilles unifiées (mono, point d'état, liseré).\n Aucune propriété `order` (règle Groupe Ka : ordre DOM = ordre visuel).\n ========================================================================== */\n\n/* --- Blocs de la fiche : cartes posées sur le papier --- */\n.f-bloc:not(.f-galerie):not(.f-hero) {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-card);\n padding: 18px 20px 16px;\n box-shadow: var(--shadow-off-soft);\n}\n@media (max-width: 640px) {\n .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n}\n\n/* --- Titres de section : carré accent + filet éditorial --- */\n.f-bloc h2, .quartier h2 {\n display: flex; align-items: center; gap: 10px;\n font-size: 19px; letter-spacing: -0.015em; margin-bottom: 14px;\n}\n.f-bloc h2::before, .quartier h2::before {\n content: \"\"; flex: 0 0 9px; width: 9px; height: 9px;\n background: var(--accent); border: 1.5px solid var(--ink);\n box-shadow: 2px 2px 0 var(--ink);\n}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }\n.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--ink);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 34px;\n line-height: 1; color: var(--ink);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n","structuredPatch":[{"oldStart":2257,"oldLines":10,"newStart":2257,"newLines":13,"lines":[" }"," "," /* --- Vie quotidienne en hiver --- */","-.hiver-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }","+.hiver-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }"," .hiver-score {","- font-family: var(--font-display); font-weight: 700; font-size: 34px;","+ font-family: var(--font-display); font-weight: 700; font-size: 30px;"," line-height: 1; color: var(--ink);","+ background: var(--surface); border: 1.5px solid var(--ink);","+ border-radius: var(--r-ctl); padding: 8px 12px;","+ box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);"," }"," .hiver-sur { font-size: 12.5px; color: var(--ink-3); }"," .hiver-detail {"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c451833e-b6a1-45bb-bd73-0d4cf73c1d9a","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMex7jLfxPUhaBiRB9i","type":"message","role":"assistant","content":[{"type":"text","text":"Maintenant la note du gestionnaire en tuile sharp + les media queries mobile."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2670,"cache_read_input_tokens":107477,"cache_creation":{"ephemeral_5m_input_tokens":2670,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0f43dd5a-cff6-4e5e-a9a1-6be8e3566f80"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMex7jLfxPUhaBiRB9i","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ExoYDE1BsR984mt9nhjC2d","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","old_string":".gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}","new_string":".gest-google { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 20px;\n color: var(--accent-deep, #cc5500);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 5px 10px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n white-space: nowrap;\n}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2670,"cache_read_input_tokens":107477,"cache_creation":{"ephemeral_5m_input_tokens":2670,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cfe2c2a2-f459-4717-8822-34ba202b3976"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ExoYDE1BsR984mt9nhjC2d","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"43ff877c-140b-461e-8fe0-7a4f02763a6d","timestamp":"2026-08-24T22:24:24.227Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","oldString":".gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}","newString":".gest-google { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 20px;\n color: var(--accent-deep, #cc5500);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 5px 10px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n white-space: nowrap;\n}","originalFile":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }\n\n/* Garde-fou : jamais de défilement horizontal de la page, même transitoire\n pendant le chargement (bandeau défilant, polices) — clip ne casse pas\n position: sticky, contrairement à hidden. */\nhtml, body { overflow-x: clip; }\n\n/* ================= Header ================= */\n.header {\n position: sticky; top: 0; z-index: var(--z-header, 500);\n background: var(--paper);\n border-bottom: 1px solid var(--line);\n}\n.header-inner { display: flex; align-items: center; gap: 20px; height: 64px; }\n.brand {\n font-family: var(--font-display); font-weight: 700; font-size: 25px;\n letter-spacing: -0.03em; display: flex; align-items: center; line-height: 1;\n color: var(--navy);\n}\n.brand .logo-ico { margin-right: 12px; flex: none; } /* ≈ 40 % de la largeur de l'icône */\n.brand .ka { color: var(--orange); }\n.brand-tag {\n font-size: 11px; font-weight: 500; color: var(--ink-3);\n letter-spacing: 0.02em; margin-left: 12px;\n}\n@media (max-width: 900px) { .brand-tag { display: none; } }\n.nav { margin-left: auto; display: flex; gap: 4px; }\n.nav a {\n padding: 9px 16px; border-radius: var(--r-pill); font-weight: 600; font-size: 14px;\n color: var(--navy); transition: all 0.15s ease; min-height: 40px;\n display: inline-flex; align-items: center;\n}\n.nav a:hover { color: var(--orange); background: var(--orange-soft); }\n.nav a.active { background: var(--navy); color: var(--white); }\n\n/* ================= Ticker ================= */\n.ticker {\n background: var(--navy); color: rgba(255, 255, 255, 0.85); overflow: hidden;\n font-size: 11.5px; font-weight: 500; letter-spacing: 0.08em;\n text-transform: uppercase; padding: 7px 0; white-space: nowrap;\n}\n.ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }\n.ticker span { padding: 0 26px; position: relative; }\n.ticker span::after { content: \"◆\"; position: absolute; right: -6px; color: var(--orange); opacity: 0.8; font-size: 8px; top: 3px; }\n@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) {\n .ticker-track { animation: none; }\n * { transition-duration: 0.01ms !important; }\n}\n\n/* ================= Hero ================= */\n.hero { padding: 58px 0 22px; }\n@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n.hero h1 {\n font-size: clamp(36px, 5.4vw, 56px); font-weight: 700; line-height: 1.06;\n letter-spacing: -0.03em; max-width: 900px; margin-top: 14px; color: var(--navy);\n}\n.hero h1 .outline { color: var(--navy); }\n.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }\n.stat-chip {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 8px 16px; font-size: 12.5px; font-weight: 500;\n color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n box-shadow: var(--shadow-flat);\n}\n.stat-chip b { color: var(--navy); font-weight: 700; }\n.stat-chip .pulse {\n width: 8px; height: 8px; border-radius: 50%; background: var(--orange);\n box-shadow: 0 0 0 4px var(--orange-soft); animation: pulse 2.4s ease infinite;\n}\n@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 106, 0, 0.18); } }\n\n/* ================= Rangée d'avantages ================= */\n.benefits {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px; margin-top: 30px;\n}\n.benefit { display: flex; align-items: center; gap: 12px; }\n.benefit-ico {\n width: 42px; height: 42px; border-radius: var(--r-pill); flex: none;\n display: flex; align-items: center; justify-content: center;\n background: var(--orange-soft); color: var(--orange);\n}\n.benefit-ico.blue { background: var(--blue-soft); color: var(--blue); }\n.benefit-label {\n font-size: 11.5px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--navy);\n}\n@media (max-width: 640px) {\n .benefits { grid-template-columns: 1fr 1fr; gap: 12px; }\n}\n\n/* ================= Filter bar ================= */\n.filterbar {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 14px; margin: 30px 0 6px;\n display: flex; flex-direction: column; gap: 0; min-width: 0;\n}\n\n/* — rangée principale — */\n.f-primary { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; min-width: 0; }\n.f-search {\n flex: 1 1 220px; min-width: 0; max-width: 440px; align-self: center;\n display: flex; align-items: center; gap: 8px;\n border: 1px solid transparent; border-radius: var(--r-pill); background: var(--grey);\n padding: 0 15px; min-height: 42px; color: var(--ink-2);\n transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;\n}\n.f-search:focus-within {\n background: var(--white); border-color: var(--blue);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n.f-search input {\n border: none; background: none; outline: none; flex: 1; min-width: 0;\n font-size: 14px; color: var(--ink); font-family: inherit;\n}\n.f-clear {\n border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n width: 20px; height: 20px; font-size: 10px; cursor: pointer; flex: none;\n display: grid; place-items: center;\n}\n.f-ctl {\n flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center;\n gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n padding: 7px 14px 6px; min-height: 52px; cursor: pointer;\n transition: box-shadow 0.15s ease, border-color 0.15s ease;\n}\n.f-ctl:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.f-ctl > span {\n font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-ctl select {\n border: none; background: transparent; outline: none; font-family: var(--font-display);\n font-weight: 700; font-size: 14.5px; color: var(--navy); cursor: pointer;\n appearance: none; -webkit-appearance: none; padding-right: 16px; min-width: 0; max-width: 170px;\n text-overflow: ellipsis;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right center;\n}\n.range-pair { display: flex; align-items: center; gap: 4px; }\n.range-pair select { max-width: 86px; }\n.range-sep { color: var(--ink-3); font-size: 11px; }\n.f-more {\n flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n transition: all 0.13s ease;\n}\n.f-more:hover { background: var(--blue-soft); border-color: var(--blue); color: var(--blue); }\n.f-more.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* — panneau avancé — */\n.f-adv {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 22px;\n border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n animation: adv-in 0.18s ease;\n}\n@keyframes adv-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }\n.f-group { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.f-group > label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-group-end { justify-content: flex-end; }\n.f-group .btn:disabled { opacity: 0.4; cursor: default; }\n.f-native {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 10px 30px 10px 14px; font-size: 14px; color: var(--ink); outline: none;\n font-family: inherit; min-height: 42px; width: 100%; min-width: 0;\n appearance: none; -webkit-appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center;\n}\n.f-native:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); background: var(--white); }\n\n/* segments (pilules soudées) */\n.seg { display: inline-flex; flex-wrap: wrap; row-gap: 6px; }\n.seg button {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink-2);\n padding: 8px 14px; font-family: var(--font-display); font-weight: 600; font-size: 13px;\n cursor: pointer; margin-left: -1px; white-space: nowrap; min-height: 38px;\n transition: all 0.12s ease;\n}\n.seg button:first-child { border-radius: var(--r-pill) 0 0 var(--r-pill); margin-left: 0; padding-left: 16px; }\n.seg button:last-child { border-radius: 0 var(--r-pill) var(--r-pill) 0; padding-right: 16px; }\n.seg button:hover { background: var(--blue-soft); color: var(--blue); }\n.seg button.on { background: var(--blue); border-color: var(--blue); color: var(--white); position: relative; z-index: 1; }\n\n/* pastilles de filtres actifs */\n.pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }\n.pill {\n display: inline-flex; align-items: center; gap: 7px;\n border: 1px solid rgba(28, 92, 65, 0.3); background: var(--blue-soft); color: var(--blue);\n border-radius: var(--r-pill); padding: 6px 14px; font-size: 12.5px; font-weight: 600;\n cursor: pointer; transition: all 0.13s ease;\n}\n.pill:hover { background: var(--blue); color: var(--white); }\n.pill-x { font-size: 10px; opacity: 0.65; }\n.pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }\n.pill-clear:hover { background: var(--danger); color: #fff; }\n.chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 4px 4px; flex: none; }\n.link-btn {\n background: none; border: none; padding: 0; color: var(--blue); font: inherit;\n text-decoration: underline; cursor: pointer;\n}\n.field { display: flex; flex-direction: column; gap: 5px; }\n.field label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); padding-left: 2px;\n}\n.field input, .field select {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 11px 14px; font-size: 15px; color: var(--ink); outline: none; font-family: inherit;\n transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; min-height: 44px;\n appearance: none; -webkit-appearance: none;\n}\n.field select {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px;\n}\n.field input:focus, .field select:focus {\n border-color: var(--blue); background: var(--white);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n/* .btn / .btn-primary / .btn-ghost : fournis par tokens.css (ka-ui). */\n\n/* ================= Chips ================= */\n.chips { display: flex; gap: 8px; margin: 16px 0 4px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }\n.chips::-webkit-scrollbar { display: none; }\n.chip {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink);\n border-radius: var(--r-pill); padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer;\n font-family: var(--font-display); white-space: nowrap; min-height: 40px;\n transition: transform 0.13s, box-shadow 0.13s, background 0.13s, color 0.13s;\n}\n.chip:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.chip.on { background: var(--ink); border-color: var(--ink); color: var(--accent); box-shadow: 3px 3px 0 var(--accent); }\n\n/* ================= Results ================= */\n.results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }\n.results-head h2 { font-size: 22px; letter-spacing: -0.02em; color: var(--navy); }\n.results-head span { color: var(--ink-3); font-size: 12.5px; font-weight: 500; }\n.grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));\n gap: 22px; padding-bottom: 70px;\n}\n@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n\n.card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-flat);\n transition: transform 0.16s ease, box-shadow 0.16s ease;\n}\n.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-off); }\n.card:focus-visible { outline: 3px solid rgba(28, 92, 65, 0.4); outline-offset: 2px; }\n.card-img { position: relative; aspect-ratio: 16/10.5; background: var(--grey); overflow: hidden; }\n.card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }\n.card:hover .card-img img { transform: scale(1.05); }\n.card-img .noimg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--ink-3); font-size: 34px; }\n.badge {\n position: absolute; top: 12px; left: 12px; border-radius: var(--r-pill); padding: 4px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: 0.04em;\n background: rgba(255, 255, 255, 0.94); color: var(--navy);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.badge.type { background: var(--orange); color: var(--white); }\n.badge.right { left: auto; right: 12px; background: rgba(255, 255, 255, 0.92); }\n.card-body { padding: 16px 18px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\n.card-price { font-family: var(--font-display); font-weight: 700; font-size: 21px; letter-spacing: -0.02em; color: var(--navy); }\n.card-price small { font-weight: 500; color: var(--ink-3); font-size: 11.5px; }\n.card-title { font-weight: 600; font-size: 14.5px; color: var(--ink); }\n.card-meta { color: var(--ink-3); font-size: 12.5px; display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }\n.card-meta .sep { width: 4px; height: 4px; background: var(--orange); border-radius: 50%; }\n.card-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.source-tag {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--blue); background: var(--blue-soft);\n border-radius: var(--r-pill); padding: 3px 10px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;\n}\n.avail { font-size: 11.5px; color: var(--ink-2); font-weight: 500; text-align: right; }\n\n/* ================= Skeletons ================= */\n@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }\n.skel { border-radius: var(--r-card); border: 1.5px solid var(--ink); overflow: hidden; background: var(--surface); }\n.skel .sk-img, .skel .sk-line {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.skel .sk-img { aspect-ratio: 16/10.5; }\n.skel .sk-line { height: 14px; border-radius: 4px; margin: 12px 16px; }\n.skel .sk-line.short { width: 45%; }\n\n/* ================= Empty / error ================= */\n.notice { text-align: center; padding: 72px 24px; color: var(--ink-2); }\n.notice .big { font-size: 44px; margin-bottom: 10px; }\n.notice h2 { color: var(--navy); }\n\n/* ================= Detail page ================= */\n.detail { padding: 30px 0 90px; }\n.crumbs {\n font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;\n color: var(--ink-3); margin-bottom: 20px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;\n}\n.crumbs a { border-bottom: 1.5px solid transparent; }\n.crumbs a:hover { color: var(--blue); border-color: var(--blue); }\n.detail-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n/* les enfants de grille ont min-width:auto par défaut : un contenu large\n (grille du quartier) déborderait de l'écran mobile sans ceci */\n.detail-grid > * { min-width: 0; }\n\n.gallery { display: flex; flex-direction: column; gap: 10px; }\n.gallery-main {\n border-radius: var(--r-card); overflow: hidden; border: 1.5px solid var(--ink);\n aspect-ratio: 16/10; background: var(--grey); cursor: zoom-in; box-shadow: var(--shadow-off-soft);\n}\n.gallery-main img { width: 100%; height: 100%; object-fit: cover; }\n.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; }\n.thumbs button {\n border: 2px solid var(--line); border-radius: 12px; overflow: hidden; padding: 0; cursor: pointer;\n aspect-ratio: 4/3; background: var(--grey); transition: border-color 0.12s ease, transform 0.12s ease;\n}\n.thumbs button:hover { transform: translateY(-2px); }\n.thumbs button.on { border-color: var(--orange); }\n.thumbs img { width: 100%; height: 100%; object-fit: cover; }\n\n.panel {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px; position: sticky; top: 120px;\n}\n@media (max-width: 900px) { .panel { position: static; } }\n.panel .price { font-family: var(--font-display); font-size: 34px; font-weight: 700; letter-spacing: -0.03em; color: var(--navy); }\n.panel .price small { font-size: 12.5px; color: var(--ink-3); font-weight: 500; }\n.panel h1 { font-size: 22px; margin: 8px 0 2px; }\n.panel .loc { color: var(--ink-2); font-size: 14px; }\n.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n/* .klabel : fourni par tokens.css (ka-ui). */\n.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n.amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }\n.amenity {\n background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 600;\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-pill); padding: 5px 13px;\n}\n.cta {\n display: block; text-align: center; background: var(--orange); color: var(--white);\n font-weight: 600; font-family: var(--font-display); border-radius: var(--r-pill);\n padding: 15px; min-height: 48px;\n box-shadow: 0 8px 22px rgba(255, 106, 0, 0.34); transition: all 0.14s ease;\n}\n.cta:hover { background: var(--orange-deep); }\n.cta:active { transform: translateY(1px); }\n.panel .fine { font-size: 11.5px; color: var(--ink-3); margin-top: 14px; text-align: center; }\n\n/* ================= Lightbox ================= */\n.lightbox {\n position: fixed; inset: 0; background: rgba(20, 24, 20, 0.94); z-index: var(--z-modal, 900);\n display: flex; align-items: center; justify-content: center; cursor: zoom-out;\n padding: max(16px, env(safe-area-inset-top)) 16px;\n}\n.lightbox img { max-width: 94vw; max-height: 90vh; border-radius: 12px; }\n\n/* Lightbox v2 : balayage entre photos (scroll-snap) + pincement pour zoomer */\n.lightbox-v2 { padding: 0; cursor: default; display: block; }\n.lightbox-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%;\n -webkit-overflow-scrolling: touch; scrollbar-width: none;\n touch-action: pan-x pinch-zoom;\n}\n.lightbox-track::-webkit-scrollbar { display: none; }\n.lightbox-cell {\n flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center;\n display: flex; align-items: center; justify-content: center;\n padding: max(52px, env(safe-area-inset-top)) 10px max(24px, env(safe-area-inset-bottom));\n overflow: hidden;\n}\n.lightbox-cell img {\n max-width: 100%; max-height: 100%; border-radius: 10px;\n user-select: none; -webkit-user-drag: none; will-change: transform;\n}\n.lightbox-close {\n position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.4);\n background: rgba(20, 24, 20, 0.6); color: #fff; font-size: 18px; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.lightbox-count {\n position: fixed; left: 50%; transform: translateX(-50%); right: auto;\n top: max(22px, env(safe-area-inset-top)); bottom: auto; z-index: 3;\n}\n.lightbox-v2 .carousel-nav { position: fixed; z-index: 3; }\n\n/* visuel de secours (SmartImg) directement sous .carousel (galerie vide) */\n.carousel > img { display: block; width: 100%; aspect-ratio: 16/11; object-fit: cover; }\n\n/* ================= Sources page ================= */\n.sources { padding: 44px 0 90px; }\n.sources h1 { font-size: clamp(28px, 4vw, 40px); margin: 10px 0 6px; color: var(--navy); }\n.sources .sub { color: var(--ink-2); margin-bottom: 30px; max-width: 700px; }\n.src-wrap { overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); background: var(--surface); }\n.src-table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 720px; }\n.src-table th {\n text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: 0.1em; color: var(--ink-3); padding: 14px 18px;\n border-bottom: 1px solid var(--line-strong); background: var(--grey);\n}\n.src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }\n.src-table tr:last-child td { border-bottom: none; }\n.src-table tr:hover td { background: var(--navy-soft); }\n.src-table a { color: var(--blue); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.pill { display: inline-block; border-radius: var(--r-pill); padding: 3px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }\n.pill.ok { background: var(--blue-soft); color: var(--blue); }\n.pill.todo { background: var(--orange-soft); color: var(--orange-deep); }\n.count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--navy); }\n\n/* ================= Page Statistiques ================= */\n.stats-page { padding: 44px 0 90px; }\n.stats-title { font-size: clamp(30px, 4.6vw, 46px); margin: 10px 0 6px; color: var(--navy); }\n.stats-page .sub { color: var(--ink-2); max-width: 640px; margin-bottom: 30px; }\n\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 30px; }\n.tile {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n padding: 18px 20px; box-shadow: var(--shadow-flat);\n}\n.tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--navy); }\n.tile-k { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }\n.hero-tile { background: var(--navy); color: var(--white); border-color: var(--navy); }\n.hero-tile .tile-v { color: var(--white); }\n.hero-tile .tile-k { color: rgba(255, 255, 255, 0.6); }\n\n.viz-card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px 28px 20px; margin-bottom: 22px;\n}\n.viz-card h2 { font-size: 19px; letter-spacing: -0.01em; color: var(--navy); }\n.viz-sub { font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 4px 0 20px; }\n.viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n\n/* Histogramme (mono-série, bleu vif) */\n.histo { display: flex; align-items: stretch; gap: 2px; height: 240px; }\n.histo-col { flex: 1; display: flex; flex-direction: column; min-width: 0; cursor: default; }\n.histo-bar-zone { flex: 1; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line-strong); }\n.histo-bar {\n width: 100%; background: var(--blue); border-radius: 6px 6px 0 0;\n min-height: 2px; transition: background 0.12s ease;\n}\n.histo-col:hover .histo-bar { background: var(--orange); }\n.histo-x { font-size: 9.5px; color: var(--ink-3); text-align: left; height: 18px; padding-top: 5px; overflow: visible; white-space: nowrap; }\n\n/* Barres horizontales (mono-série) */\n.hbars { display: flex; flex-direction: column; gap: 7px; }\n.hbar-row {\n display: grid; grid-template-columns: minmax(96px, 170px) 1fr auto;\n gap: 12px; align-items: center; min-height: 26px; cursor: default;\n}\n.hbar-label { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.hbar-label a { border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.hbar-label a:hover { color: var(--blue); }\n.hbar-track { background: var(--grey); border-radius: var(--r-pill); height: 18px; overflow: hidden; }\n.hbar-fill {\n display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill);\n min-width: 2px; transition: background 0.12s ease;\n}\n.hbar-row:hover .hbar-fill { background: var(--orange); }\n.hbar-value { font-size: 11.5px; font-weight: 700; white-space: nowrap; color: var(--navy); }\n.hbar-value em { font-style: normal; font-weight: 500; color: var(--ink-3); }\n@media (max-width: 640px) {\n .hbar-row { grid-template-columns: minmax(80px, 110px) 1fr auto; gap: 8px; }\n .hbar-value em { display: none; }\n .viz-card { padding: 20px 16px 14px; }\n .histo { height: 170px; }\n}\n\n/* Infobulle */\n.viz-tip {\n position: fixed; z-index: var(--z-toast, 950); pointer-events: none; max-width: 180px;\n background: var(--navy); color: var(--white); border-radius: 12px;\n padding: 9px 12px; font-size: 12px; line-height: 1.5;\n box-shadow: 0 8px 24px rgba(20, 24, 20, 0.35);\n}\n.viz-tip-title { font-family: var(--font-display); font-weight: 700; color: var(--orange); margin-bottom: 2px; }\n\n/* Vue tableau repliable (accessibilité / relief) */\n.viz-table { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 10px; }\n.viz-table summary {\n cursor: pointer; font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em; color: var(--blue);\n}\n.viz-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }\n.viz-table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line-strong); }\n.viz-table td { padding: 6px 10px; border-bottom: 1px solid var(--line); }\n\n.stats-foot { font-size: 11.5px; color: var(--ink-3); letter-spacing: 0.02em; margin-top: 6px; }\n\n/* ================= Mobile : feuille de filtres + FAB ================= */\n.sheet-head { display: none; }\n.sheet-apply { display: none; }\n.sheet-backdrop { display: none; }\n.fab { display: none; }\n\n@media (max-width: 640px) {\n /* En-tête compact */\n .header-inner { height: 56px; }\n .brand { font-size: 21px; }\n .nav a { padding: 8px 13px; font-size: 13.5px; }\n .ticker { font-size: 10.5px; padding: 6px 0; }\n\n /* Héro resserré + stats en rangée défilante */\n .hero h1 { font-size: clamp(30px, 8.6vw, 42px); }\n .hero p.lede { font-size: 15px; }\n .stat-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 6px; margin-right: -16px; padding-right: 16px; }\n .stat-row::-webkit-scrollbar { display: none; }\n .stat-chip { flex: 0 0 auto; white-space: nowrap; }\n\n /* La barre de filtres devient une feuille coulissante (bottom sheet) */\n .filterbar { display: none; }\n .filterbar.open .f-primary { flex-direction: column; }\n .filterbar.open .f-search { align-self: stretch; max-width: none; min-height: 46px; }\n .filterbar.open .f-search input { font-size: 16px; } /* anti-zoom iOS */\n .filterbar.open .f-ctl select { max-width: none; width: 100%; }\n .filterbar.open .f-more { display: none; }\n .filterbar.open .f-adv { margin-top: 4px; }\n .filterbar.open {\n display: flex; flex-direction: column; gap: 12px;\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);\n margin: 0; border-radius: 24px 24px 0 0; border: none;\n max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;\n padding: 16px 18px calc(18px + env(safe-area-inset-bottom));\n box-shadow: 0 -16px 48px rgba(20, 24, 20, 0.28);\n animation: sheet-up 0.22s ease;\n }\n @keyframes sheet-up { from { transform: translateY(30%); opacity: 0.4; } to { transform: none; opacity: 1; } }\n .filterbar.open .sheet-head {\n display: flex; justify-content: space-between; align-items: center;\n font-family: var(--font-display); font-weight: 700; font-size: 17px;\n letter-spacing: -0.01em; color: var(--navy);\n position: sticky; top: -16px; background: var(--surface); padding: 6px 0 8px;\n border-bottom: 1px solid var(--line); margin-bottom: 2px; z-index: 1;\n }\n .sheet-close {\n border: 1.5px solid var(--ink); background: var(--surface); border-radius: 50%;\n width: 36px; height: 36px; font-size: 15px; cursor: pointer; line-height: 1; color: var(--navy);\n }\n .filterbar.open .sheet-apply { display: block; width: 100%; }\n .sheet-backdrop {\n display: block; position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.45); backdrop-filter: blur(2px);\n }\n\n /* Bouton flottant */\n .fab {\n display: flex; align-items: center; gap: 6px;\n position: fixed; left: 50%; transform: translateX(-50%);\n bottom: calc(74px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: 80;\n background: var(--orange); color: var(--white); border: none;\n border-radius: var(--r-pill); padding: 13px 24px; font-family: var(--font-display);\n font-weight: 600; font-size: 15px; cursor: pointer;\n box-shadow: 0 10px 28px rgba(255, 106, 0, 0.4);\n }\n .fab:active { transform: translateX(-50%) scale(0.97); }\n\n /* Fiche logement : vignettes en bande défilante + panneau non collant */\n .thumbs { display: flex; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }\n .thumbs::-webkit-scrollbar { display: none; }\n .thumbs button { flex: 0 0 96px; }\n .detail { padding-top: 20px; }\n .panel { padding: 20px; }\n .panel .price { font-size: 28px; }\n .results-head { margin-top: 20px; }\n .chips { margin-right: -16px; padding-right: 16px; }\n .notice { padding: 48px 16px; }\n}\n\n/* ================= Barre de navigation inférieure (mobile) ================= */\n.tabbar { display: none; }\n@media (max-width: 760px) {\n .tabbar {\n display: grid; grid-template-columns: repeat(4, 1fr);\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border-top: 1px solid var(--line);\n padding: 6px 4px calc(6px + env(safe-area-inset-bottom));\n }\n .tabbar a {\n display: flex; flex-direction: column; align-items: center; gap: 3px;\n padding: 5px 2px; min-height: 48px; justify-content: center;\n font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n border-radius: var(--r-ctl); transition: color 0.12s ease;\n }\n .tabbar a.active { color: var(--orange); }\n .tabbar a svg { display: block; }\n body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }\n .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }\n}\n\n/* Anti-zoom iOS : les champs doivent faire >= 16px\n (filet global : tokens.css force aussi 16 px au doigt) */\n@media (max-width: 900px) {\n .field input, .field select,\n .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }\n}\n\n/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */\n@media (pointer: coarse) {\n .brand { position: relative; }\n .brand::after { content: \"\"; position: absolute; inset: -8px; }\n}\n\n/* ================= Footer ================= */\n.footer { background: var(--navy); color: rgba(255, 255, 255, 0.72); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; }\n.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 32px; color: var(--white); letter-spacing: -0.03em; margin-bottom: 4px; display: flex; align-items: center; }\n.footer .fbrand .logo-ico { margin-right: 13px; }\n.footer .fbrand .ka { color: var(--orange); }\n.footer .fbaseline { color: rgba(255, 255, 255, 0.65); font-size: 14px; margin-bottom: 16px; }\n.footer .fbaseline .hl { color: var(--orange); font-weight: 600; }\n.footer b { color: var(--white); }\n.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }\n.footer .fmono { font-size: 11.5px; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.42); margin-top: 18px; }\n\n/* --- Carte interactive (mission 4) --------------------------------------- */\n.results-tools { display: flex; align-items: center; gap: 14px; }\n.view-toggle {\n display: inline-flex; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); overflow: hidden; background: var(--surface);\n box-shadow: var(--shadow-flat);\n}\n.view-toggle button {\n border: 0; background: transparent; padding: 7px 16px; cursor: pointer;\n font-size: 12.5px; font-weight: 600; color: var(--ink-2);\n}\n/* zone tactile >= 44 px au doigt (coarse pointer = mobile/tablette) */\n@media (pointer: coarse) {\n .view-toggle button { min-height: 44px; padding: 10px 18px; }\n}\n.view-toggle button + button { border-left: 1px solid var(--line); }\n.view-toggle button.on { background: var(--blue); color: var(--white); }\n\n.map-split {\n display: grid; grid-template-columns: minmax(300px, 400px) 1fr; gap: 18px;\n height: calc(100dvh - 170px); min-height: 420px;\n}\n.map-list {\n overflow-y: auto; display: flex; flex-direction: column; gap: 14px;\n padding-right: 4px; scrollbar-width: thin;\n}\n.map-list .card { margin: 0; flex: 0 0 auto; }\n.mapview {\n width: 100%; height: 100%; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.map-loading {\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-3); font-size: 13px;\n}\n\n/* mobile d'abord : carte plein écran, liste masquée (bascule via l'onglet) */\n@media (max-width: 780px) {\n .map-split { grid-template-columns: 1fr; height: calc(100dvh - 210px); }\n .map-list { display: none; }\n .results-tools { width: 100%; justify-content: space-between; }\n}\n\n/* popup MapLibre au style Lou-Ka */\n.maplibregl-popup-content {\n padding: 0; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); box-shadow: var(--shadow-off);\n font-family: var(--font-body);\n}\n.maplibregl-popup-close-button {\n font-size: 18px; padding: 2px 8px; color: var(--white); z-index: 2;\n text-shadow: 0 0 4px rgba(20, 24, 20, 0.8);\n}\n.mv-pop img, .mv-noimg { width: 100%; height: 130px; object-fit: cover; }\n.mv-noimg { display: flex; align-items: center; justify-content: center;\n font-size: 34px; background: var(--grey); }\n.mv-pop-body { padding: 10px 12px 12px; }\n.mv-pop-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--navy); }\n.mv-pop-price small { font-weight: 400; color: var(--ink-3); font-size: 12px; }\n.mv-pop-title { font-size: 13px; color: var(--ink-2); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mv-pop-meta { font-size: 11.5px; color: var(--ink-2); margin-top: 6px; }\n.mv-pop-src { font-size: 10px; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }\n.mv-pop-cta {\n display: block; margin-top: 10px; padding: 9px 10px; text-align: center;\n background: var(--orange); color: var(--white); border-radius: var(--r-pill);\n font-weight: 600; font-size: 13px;\n}\n.mv-pop-cta:hover { background: var(--orange-deep); }\n\n/* --- Commodités de proximité (fiche) -------------------------------------- */\n.poi-list {\n list-style: none; margin: 0; padding: 0;\n display: flex; flex-direction: column; gap: 2px;\n}\n.poi-list li {\n display: flex; align-items: center; gap: 9px;\n padding: 5px 2px; border-bottom: 1px solid var(--line);\n font-size: 13px;\n}\n.poi-list li:last-child { border-bottom: 0; }\n.poi-ico { width: 20px; text-align: center; flex: 0 0 auto; }\n.poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.poi-dist { font-size: 11.5px; color: var(--navy); font-weight: 600; flex: 0 0 auto; }\n\n/* --- Menu déroulant mobile ------------------------------------------------- */\n.menu-btn {\n display: none; position: relative; z-index: 60;\n width: 44px; height: 44px; margin-left: auto;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); cursor: pointer; padding: 0;\n flex-direction: column; align-items: center; justify-content: center; gap: 5px;\n box-shadow: var(--shadow-flat);\n transition: box-shadow 0.15s ease, transform 0.15s ease;\n}\n.menu-btn:active { transform: scale(0.96); }\n.menu-btn span {\n display: block; width: 18px; height: 2px; background: var(--navy);\n border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease;\n}\n.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }\n.menu-btn.open span:nth-child(2) { opacity: 0; }\n.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }\n\n.mobile-menu {\n display: none; position: absolute; left: 0; right: 0; top: 100%;\n background: var(--white); border-bottom: 1px solid var(--line);\n box-shadow: 0 18px 30px rgba(20, 24, 20, 0.14);\n padding: 8px 16px calc(16px + env(safe-area-inset-bottom));\n max-height: 0; overflow: hidden; opacity: 0;\n transition: max-height 0.3s ease, opacity 0.22s ease;\n}\n.mobile-menu { z-index: var(--z-modal, 900); }\n.mobile-menu.open { max-height: 480px; opacity: 1; }\n/* Le drawer vit DANS le header : quand il est ouvert, le header monte au niveau\n modal pour peindre au-dessus du .mm-backdrop (frère du header, overlay 800). */\n.header:has(.mobile-menu.open) { z-index: var(--z-modal, 900); }\n.mm-link {\n display: flex; align-items: center; gap: 12px;\n padding: 15px 10px; min-height: 52px;\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n letter-spacing: -0.02em; border-bottom: 1px solid var(--line);\n color: var(--navy);\n opacity: 0; transform: translateY(-8px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); }\n.mm-link.active { color: var(--orange); }\n.mm-link.active .mm-arrow { opacity: 1; color: var(--orange); }\n.mm-ico { width: 26px; text-align: center; font-size: 17px; }\n.mm-arrow { margin-left: auto; opacity: 0.25; color: var(--orange); transition: opacity 0.15s ease; }\n.mm-link:active { background: var(--orange-soft); border-radius: var(--r-ctl); }\n.mm-foot {\n padding: 12px 10px 4px;\n font-size: 11px; color: var(--ink-3); letter-spacing: 0.02em;\n}\n.mm-backdrop {\n position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px);\n}\n@media (max-width: 760px) {\n .menu-btn { display: flex; }\n .nav { display: none; }\n .mobile-menu { display: block; }\n}\n\n/* --- Bandeau de consentement (témoins) ------------------------------------- */\n.cookie-banner {\n position: fixed; left: 12px; right: 12px;\n bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80;\n animation: cookie-in 0.35s ease;\n}\n@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } }\n.cookie-inner {\n max-width: 720px; margin: 0 auto; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 16px 18px;\n}\n.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }\n.cookie-text b { color: var(--navy); }\n.cookie-text a { text-decoration: underline; text-underline-offset: 2px; }\n.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n.cookie-opt {\n display: flex; align-items: flex-start; gap: 10px; font-size: 13px;\n color: var(--ink-2); cursor: pointer;\n}\n.cookie-opt input { margin-top: 2px; accent-color: var(--orange); width: 16px; height: 16px; }\n.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; }\n@media (max-width: 640px) {\n .cookie-actions { justify-content: stretch; }\n .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; }\n}\n\n/* --- Page confidentialité + lien pied de page ------------------------------- */\n.legal { padding: 48px 0 70px; max-width: 760px; }\n.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; color: var(--navy); }\n.legal section { margin-top: 26px; }\n.legal h3 { font-size: 18px; margin-bottom: 8px; }\n.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }\n.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }\n.flink {\n border: 0; background: none; padding: 0; cursor: pointer;\n color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px;\n}\n\n/* --- Section « Le quartier » (fiche) --------------------------------------- */\n.quartier { margin-top: 34px; }\n.quartier h2 { font-size: 24px; letter-spacing: -0.02em; color: var(--navy); }\n.q-sub { color: var(--ink-3); font-size: 13px; margin: 4px 0 16px; }\n.q-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;\n}\n@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n.q-cell {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 12px 14px; box-shadow: var(--shadow-flat);\n}\n.q-val { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.q-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px; }\n.q-prox { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }\n.q-bar { display: flex; align-items: center; gap: 10px; font-size: 13px; }\n.q-bar-label { flex: 0 0 150px; color: var(--ink-2); }\n@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n.q-bar-track {\n flex: 1; height: 10px; background: var(--grey);\n border-radius: var(--r-pill); overflow: hidden;\n}\n.q-bar-fill { display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill); }\n.q-bar-num { flex: 0 0 30px; text-align: right;\n font-size: 11.5px; font-weight: 700; color: var(--navy); }\n.q-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.q-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 7px 14px; font-size: 12.5px; background: var(--surface);\n}\n.q-badge.cool { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.q-badge.hot { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.quartier .fine { margin-top: 10px; }\n\n/* --- Fiche v2 (mobile-first) ------------------------------------------------ */\n/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka\n « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`\n / `display:contents` pour réordonner — les blocs sans `order` retombaient à\n order:0 et passaient devant la galerie sur mobile. Mobile : colonnes\n empilées ; desktop : grid 2 colonnes (logement | synthèse). */\n.fiche { display: flex; flex-direction: column; gap: 22px; }\n.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n@media (min-width: 900px) {\n .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n .f-col { gap: 26px; }\n}\n.f-bloc { min-width: 0; }\n.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n.f-bloc:empty { display: none; }\n\n/* galerie à balayage natif */\n.carousel { position: relative; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); background: var(--grey); box-shadow: var(--shadow-off-soft); }\n.carousel-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch; scrollbar-width: none; aspect-ratio: 16/11;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track img {\n flex: 0 0 100%; width: 100%; object-fit: cover; scroll-snap-align: center;\n cursor: zoom-in;\n}\n.carousel-empty { display: flex; align-items: center; justify-content: center;\n aspect-ratio: 16/11; font-size: 48px; }\n.carousel-count {\n position: absolute; right: 12px; bottom: 12px; z-index: 2;\n background: rgba(20, 24, 20, 0.78); color: var(--white);\n font-size: 12px; font-weight: 600;\n padding: 4px 11px; border-radius: var(--r-pill);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.carousel-nav {\n position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--ink);\n background: rgba(255, 255, 255, 0.92); color: var(--navy); font-size: 22px; cursor: pointer;\n display: flex; align-items: center; justify-content: center; line-height: 1;\n box-shadow: var(--shadow-flat);\n}\n.carousel-nav.prev { left: 10px; } .carousel-nav.next { right: 10px; }\n@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n\n/* bandeau prix + badge marché */\n.f-hero .price { font-size: clamp(34px, 8vw, 44px); }\n.deal-badge {\n display: inline-flex; align-items: center; gap: 6px; margin: 8px 0 4px;\n border-radius: var(--r-pill); padding: 7px 14px; font-size: 13px; font-weight: 600;\n border: 1.5px solid var(--ink);\n}\n.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.deal-ok { background: var(--surface); color: var(--ink-2); }\n.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.f-hero h1 { font-size: clamp(20px, 4.5vw, 26px); margin-top: 8px; }\n\n/* chips clés à défilement horizontal */\n.chips-scroll {\n display: flex; gap: 8px; overflow-x: auto; padding: 12px 0 6px;\n scrollbar-width: none; -webkit-overflow-scrolling: touch;\n}\n.chips-scroll::-webkit-scrollbar { display: none; }\n.chip-key {\n flex: 0 0 auto; background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 8px 15px;\n font-size: 12px; font-weight: 600; white-space: nowrap; min-height: 34px;\n display: inline-flex; align-items: center;\n}\n\n/* ancres de navigation rapide */\n.ancres {\n display: flex; gap: 4px; overflow-x: auto; padding: 6px 0 2px;\n scrollbar-width: none; border-bottom: 1px solid var(--line); margin-bottom: 8px;\n}\n.ancres::-webkit-scrollbar { display: none; }\n.ancres a {\n flex: 0 0 auto; padding: 9px 14px; font-size: 13.5px; font-weight: 600;\n color: var(--ink-2); border-radius: var(--r-pill); min-height: 44px;\n display: inline-flex; align-items: center;\n}\n.ancres a:hover { background: var(--orange-soft); color: var(--orange); }\nhtml { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */\n\n/* description restructurée */\n.enbref {\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-card); padding: 12px 16px; color: var(--navy);\n font-size: 15px; margin: 0 0 14px;\n}\n.desc-section { margin-bottom: 12px; }\n.desc-section h4 { font-size: 15px; margin-bottom: 3px; }\n.desc-section p { color: var(--ink-2); font-size: 14.5px; margin: 0; white-space: pre-line; }\n.texte-original { margin-top: 12px; }\n.texte-original summary {\n cursor: pointer; font-size: 12.5px;\n color: var(--ink-3); min-height: 44px; display: flex; align-items: center;\n}\n.texte-original p { color: var(--ink-3); font-size: 13px; white-space: pre-line; }\n\n/* inclusions : confirmées vs mentionnées */\n.amenity.confirmed { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.25); color: var(--blue); font-weight: 600; }\n.amenity.unconfirmed { background: transparent; border-style: dashed; border-color: var(--line-strong); color: var(--ink-3); }\n\n/* détails pratiques : historique de prix */\n.prix-histo {\n margin-top: 12px; padding: 10px 14px; border-radius: var(--r-card);\n border: 1.5px solid var(--ink); font-size: 13.5px; background: var(--surface);\n}\n.prix-histo.down { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n\n/* quartier compacté sur mobile */\n@media (max-width: 640px) {\n .q-cell { padding: 9px 11px; }\n .q-val { font-size: 16px; }\n .q-bar { gap: 8px; font-size: 12px; }\n .q-bar-track { height: 8px; }\n}\n.q-badge-sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }\n\n/* POI groupés repliables */\n.poi-groupe { border: 1.5px solid var(--ink); border-radius: var(--r-card);\n margin-bottom: 8px; background: var(--surface); overflow: hidden; }\n.poi-groupe summary {\n display: flex; justify-content: space-between; align-items: center; gap: 10px;\n padding: 12px 14px; cursor: pointer; font-weight: 600; font-size: 14px;\n min-height: 48px; list-style: none;\n}\n.poi-groupe summary::-webkit-details-marker { display: none; }\n.poi-groupe summary::after { content: \"▾\"; color: var(--ink-3); transition: transform 0.15s ease; }\n.poi-groupe[open] summary::after { transform: rotate(180deg); }\n.poi-resume { font-size: 11.5px; color: var(--ink-3); font-weight: 400; }\n.poi-groupe .poi-list { padding: 0 14px 10px; }\n\n/* CTA sticky mobile */\n.cta-sticky {\n position: fixed; left: 0; right: 0; z-index: var(--z-dropdown, 700);\n bottom: var(--consent-h, 0px);\n display: flex; align-items: center; gap: 12px;\n background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(12px);\n border-top: 1px solid var(--line);\n padding: 10px 16px calc(10px + env(safe-area-inset-bottom));\n}\n.cta-sticky .cta { flex: 1; margin: 0; text-align: center; }\n.cta-sticky-prix { font-family: var(--font-display); font-weight: 700; font-size: 19px; white-space: nowrap; color: var(--navy); }\n.cta-sticky-prix small { font-weight: 500; font-size: 10px; color: var(--ink-3); }\n@media (min-width: 900px) { .cta-sticky { display: none; } }\n@media (max-width: 899px) {\n .cta-desktop { display: none; }\n .detail { padding-bottom: 84px; } /* place pour le CTA sticky */\n}\n.f-foot { margin-top: 26px; }\n\n/* --- Passerelle vers l'annonce originale ------------------------------------ */\n.passerelle {\n min-height: calc(100dvh - 120px); display: flex; align-items: center;\n justify-content: center; padding: 24px 16px;\n}\n.pass-card {\n width: 100%; max-width: 520px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 30px 28px; text-align: center;\n}\n.pass-brands {\n display: flex; align-items: center; justify-content: center; gap: 14px;\n margin-bottom: 22px; flex-wrap: wrap;\n}\n.pass-brand {\n font-family: var(--font-display); font-weight: 700; font-size: 24px;\n letter-spacing: -0.03em; display: inline-flex; align-items: center; color: var(--navy);\n}\n.pass-brand .ka { color: var(--orange); }\n.pass-trajet { display: inline-flex; align-items: center; gap: 5px; }\n.pass-dot {\n width: 6px; height: 6px; border-radius: 50%; background: var(--orange);\n opacity: 0.25; animation: pass-dot 1.2s ease infinite;\n}\n.pass-dot:nth-child(2) { animation-delay: 0.2s; }\n.pass-dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes pass-dot { 40% { opacity: 1; transform: translateX(2px); } }\n.pass-fleche { color: var(--orange); font-size: 18px; }\n.pass-gestionnaire {\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n display: inline-flex; align-items: center; gap: 8px;\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-pill); padding: 6px 16px; color: var(--blue);\n}\n.pass-card h1 { font-size: 20px; letter-spacing: -0.02em; }\n.pass-texte { color: var(--ink-2); font-size: 14.5px; margin: 10px 0 20px; }\n.pass-progress {\n height: 10px; border-radius: var(--r-pill);\n overflow: hidden; background: var(--grey); margin-bottom: 20px;\n}\n.pass-progress-fill {\n display: block; height: 100%; width: 0; background: var(--orange);\n border-radius: var(--r-pill);\n}\n.pass-progress-fill.go { animation: pass-fill 2.2s linear forwards; }\n@keyframes pass-fill { to { width: 100%; } }\n@media (prefers-reduced-motion: reduce) {\n .pass-progress-fill.go { animation: none; width: 100%; }\n .pass-dot { animation: none; opacity: 0.7; }\n}\n.pass-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; }\n\n/* --- boutons PDF -------------------------------------------------------- */\n.btn-pdf { display: block; text-align: center; margin-top: 10px; width: 100%; }\n\n/* --- Onglets de la page Stats -------------------------------------------- */\n.onglets {\n display: flex; gap: 8px; overflow-x: auto; margin: 26px 0 22px;\n padding-bottom: 4px; scrollbar-width: none;\n border-bottom: 1px solid var(--line);\n}\n.onglets::-webkit-scrollbar { display: none; }\n.onglet {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;\n border: 1.5px solid var(--ink);\n border-radius: var(--r-pill);\n background: var(--surface); color: var(--ink-2); cursor: pointer;\n padding: 10px 18px; font-weight: 600; font-size: 13.5px; min-height: 44px;\n margin-bottom: 8px;\n transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;\n}\n.onglet:hover { background: var(--blue-soft); color: var(--blue); border-color: var(--blue); }\n.onglet.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* baisses de prix */\n.baisses { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }\n.baisses li {\n display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n padding: 9px 4px; border-bottom: 1px solid var(--line); font-size: 13.5px;\n}\n.baisses li a { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }\n.baisse-ville { color: var(--ink-3); font-size: 11.5px; }\n.baisse-prix { margin-left: auto; }\n.baisse-prix s { color: var(--ink-3); }\n.baisse-pct {\n font-style: normal; font-weight: 700;\n background: var(--blue-soft);\n color: var(--blue); border-radius: var(--r-pill); padding: 2px 9px;\n font-size: 11px; margin-left: 8px;\n}\n.alertes { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n\n/* ================= Pagination (12 annonces/page) ================= */\n.pager {\n display: flex; align-items: center; justify-content: space-between;\n gap: 16px; flex-wrap: wrap; margin: 26px 0 10px; padding-top: 18px;\n border-top: 1px solid var(--line);\n}\n.pager-info {\n font-size: 12.5px; color: var(--ink-2); letter-spacing: 0.02em;\n}\n.pager-info em { font-style: normal; color: var(--ink-3); }\n.pager-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.pager-btn {\n min-width: 38px; height: 38px; padding: 0 10px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); cursor: pointer;\n font-size: 13px; font-weight: 600;\n color: var(--navy); transition: all 0.12s ease;\n}\n.pager-btn:hover:not(:disabled):not(.on) {\n background: var(--blue-soft); border-color: var(--blue); color: var(--blue);\n}\n.pager-btn.on { background: var(--navy); border-color: var(--navy); color: var(--white); cursor: default; }\n.pager-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n.pager-ellipsis { color: var(--ink-3); padding: 0 2px; }\n.map-list .pager { margin: 10px 0 4px; }\n@media (max-width: 640px) {\n .pager { justify-content: center; }\n .pager-info { width: 100%; text-align: center; order: 2; }\n}\n\n/* ================= Icônes SVG inline — alignement ================= */\n.chip-ico { display: inline-flex; align-items: center; gap: 6px; }\n.chip-ico svg { flex: none; }\n.view-toggle button, .seg button { display: inline-flex; align-items: center; gap: 6px; }\n.fab { align-items: center; gap: 8px; }\n.fab svg { flex: none; }\n.mm-ico { display: inline-flex; align-items: center; }\n.notice .big { display: flex; justify-content: center; color: var(--ink-3); }\n.notice .big svg { display: block; }\n.card-img .noimg { color: var(--ink-3); }\n.badge.right { display: inline-flex; align-items: center; gap: 4px; }\n.btn-pdf { display: inline-flex; align-items: center; gap: 7px; }\n.f-search svg { flex: none; color: var(--ink-3); }\n.mv-noimg { display: flex; align-items: center; justify-content: center; color: var(--ink-3); }\n\n/* footer — liens (Groupe KA, courriels, mentions légales) */\n.footer .frow a { color: var(--white); text-decoration: underline; text-decoration-color: rgba(255, 106, 0, 0.5); text-underline-offset: 3px; }\n.footer .frow a:hover { color: var(--orange); }\n.footer .fmono a:hover { color: var(--orange); }\n\n/* ================= Compte (connexion Google) ================= */\n.login-btn {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 8px 16px; margin-left: 10px; min-height: 38px;\n background: var(--navy); border: none;\n border-radius: var(--r-pill); font-weight: 600; font-size: 13.5px; color: var(--white);\n transition: all 0.12s ease; white-space: nowrap;\n box-shadow: 0 4px 14px rgba(20, 24, 20, 0.22);\n}\n.login-btn:hover { background: var(--blue); }\n.account { position: relative; margin-left: 10px; }\n.account-btn {\n width: 38px; height: 38px; padding: 0; border: 2px solid var(--orange);\n border-radius: var(--r-pill); overflow: hidden; cursor: pointer;\n background: var(--orange-soft); display: flex; align-items: center; justify-content: center;\n}\n.account-btn img { width: 100%; height: 100%; object-fit: cover; }\n.account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }\n/* Click-catcher plein écran rendu via createPortal à la racine de \n (un fixed descendant du header blurré se re-parentait → 66 px de haut).\n z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le\n header et doit peindre au-dessus du voile. */\n.account-backdrop { position: fixed; inset: 0; z-index: 40; }\n.account-menu {\n position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;\n min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off); overflow: hidden;\n}\n.account-id { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-bottom: 1px solid var(--line); }\n.account-id b { font-size: 14px; }\n.account-id span { font-size: 11.5px; color: var(--ink-3); word-break: break-all; }\n.account-menu button {\n width: 100%; text-align: left; padding: 11px 14px; border: 0; cursor: pointer;\n background: transparent; font-weight: 600; font-size: 13.5px; color: var(--danger);\n}\n.account-menu button:hover { background: var(--orange-soft); color: var(--navy); }\n@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n\n/* ================= Profil — carte de membre Groupe KA ================= */\n.profil { padding-top: 40px; padding-bottom: 60px; }\n.profil h1 { font-size: clamp(30px, 5vw, 44px); margin: 10px 0 26px; color: var(--navy); }\n.profil .lede { max-width: 520px; color: var(--ink-2); margin-bottom: 22px; }\n\n.pc {\n position: relative; max-width: 520px; overflow: hidden;\n background: linear-gradient(135deg, #101a40 0%, var(--navy) 55%, #080e24 100%);\n color: var(--white);\n border-radius: 20px;\n padding: 24px 26px 0; box-shadow: var(--shadow-off);\n}\n.pc-watermark {\n position: absolute; right: -18px; top: -34px; pointer-events: none;\n font-family: var(--font-display); font-weight: 700; font-size: 170px;\n letter-spacing: -0.06em; color: rgba(255, 106, 0, 0.07);\n transform: rotate(-8deg); line-height: 1;\n}\n.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }\n.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }\n.pc-ka { color: var(--orange); margin-left: 1px; display: inline-block; }\n.pc-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.5); }\n.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }\n.pc-id-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2em; color: rgba(255, 106, 0, 0.75); }\n.pc-id {\n font-weight: 700; color: var(--orange);\n font-size: clamp(22px, 6vw, 34px); letter-spacing: 0.14em;\n text-shadow: 0 0 24px rgba(255, 106, 0, 0.35);\n}\n.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }\n.pc-holder { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pc-holder-name { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pc-holder-since { font-size: 10.5px; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.06em; }\n.pc-role-badge {\n display: inline-block; width: fit-content; margin: 3px 0 2px;\n background: var(--orange); color: var(--white);\n font-size: 9px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.12em;\n padding: 2px 8px 3px; border-radius: var(--r-pill);\n}\n.pc-avatar { width: 52px; height: 52px; border-radius: var(--r-pill); border: 2px solid var(--orange); flex: none; }\n.pc-strip { display: flex; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(255, 106, 0, 0.1); border-top: 1px solid rgba(255, 106, 0, 0.25); overflow: hidden; }\n.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--orange); opacity: 0.7; }\n.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.35; }\n.pc-strip i:nth-child(3n+1) { height: 9px; }\n.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.9; }\n.pc-copy { margin-top: 16px; }\n.pc-copy.ok { background: var(--blue); color: var(--white); }\n\n.profil-grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 12px; margin-top: 30px; max-width: 760px;\n}\n.pg-item {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 14px 16px;\n display: flex; flex-direction: column; gap: 4px; box-shadow: var(--shadow-flat);\n}\n.pg-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }\n.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }\n.pg-value.mono { color: var(--blue); }\n\n.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }\n.profil-note a { text-decoration: underline; text-underline-offset: 3px; }\n.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }\n.profil-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n\n.account-kaid { font-size: 10.5px; color: var(--blue); letter-spacing: 0.08em; }\n.account-link {\n display: block; width: 100%; text-align: left; padding: 11px 14px;\n font-weight: 600; font-size: 13.5px; color: var(--navy);\n border-bottom: 1px solid var(--line);\n}\n.account-link:hover { background: var(--orange-soft); }\n\n/* profil public — section + lien partageable */\n.profil-public { margin-top: 30px; max-width: 640px; }\n.profil-public h3 { font-size: 18px; margin-bottom: 6px; }\n.profil-public p { color: var(--ink-2); font-size: 13.5px; margin: 0 0 12px; }\n.public-link {\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n margin-top: 14px; padding: 12px 14px; background: var(--blue-soft);\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-card);\n}\n.public-link a { font-size: 13px; color: var(--blue); text-decoration: underline; text-underline-offset: 3px; word-break: break-all; }\n\n/* ================= Bienvenue — choix du rôle ================= */\n.bienvenue { padding-top: 44px; padding-bottom: 70px; }\n.bienvenue h1 { font-size: clamp(30px, 5vw, 46px); margin: 10px 0 14px; color: var(--navy); }\n.bienvenue .lede { max-width: 480px; color: var(--ink-2); margin-bottom: 30px; }\n.role-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; max-width: 780px; }\n.role-card {\n display: flex; flex-direction: column; align-items: flex-start; gap: 10px;\n text-align: left; padding: 24px 24px 20px; cursor: pointer;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-flat);\n font-family: var(--font-body); transition: all 0.15s ease;\n}\n.role-card:hover:not(:disabled) { transform: translateY(-4px); box-shadow: var(--shadow-off); border-color: rgba(255, 106, 0, 0.4); }\n.role-card:disabled { opacity: 0.6; cursor: wait; }\n.role-ico { display: flex; width: 54px; height: 54px; align-items: center; justify-content: center; background: var(--orange-soft); color: var(--orange); border-radius: 16px; }\n.role-title { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.role-desc { color: var(--ink-2); font-size: 13.5px; }\n.role-perks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }\n.role-perks li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }\n.role-perks li::before {\n content: \"✓\"; flex: none; width: 16px; height: 16px; border-radius: 50%;\n background: var(--orange); color: var(--white); font-size: 10px; font-weight: 700;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.role-cta { margin-top: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--orange); }\n\n/* ================= Favoris — cœur sur les cartes ================= */\n.fav-btn {\n position: absolute; right: 10px; bottom: 10px; z-index: 2;\n width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;\n background: rgba(255, 255, 255, 0.94); border: none;\n border-radius: var(--r-pill); cursor: pointer; color: var(--navy);\n box-shadow: var(--shadow-flat);\n transition: transform 0.12s ease, background 0.12s ease, color 0.12s ease;\n}\n.fav-btn:hover { transform: scale(1.12); }\n.fav-btn.on { background: var(--white); color: var(--orange); }\n.fav-btn.on svg { filter: drop-shadow(0 0 3px rgba(255, 106, 0, 0.35)); }\n\n/* ================= Espace gestionnaire ================= */\n.claim-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; max-width: 640px; }\n.claim-row select {\n flex: 1; min-width: 260px; padding: 11px 14px; font-size: 14px;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); font-family: var(--font-body);\n}\n.claim-error { color: var(--danger); font-size: 13.5px; font-weight: 600; margin-top: 10px; }\n.org-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 22px; }\n.org-stat { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); }\n.org-form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }\n.org-form h3 { font-size: 18px; }\n.org-form label { display: flex; flex-direction: column; gap: 5px; flex: 1; }\n.org-form label span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }\n.org-form input, .org-form textarea {\n padding: 10px 14px; font-size: 14px; font-family: var(--font-body);\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);\n}\n.org-form textarea { resize: vertical; }\n.org-form input:focus, .org-form textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.org-form-row { display: flex; gap: 12px; flex-wrap: wrap; }\n.org-form .btn { align-self: flex-start; }\n\n/* ================= Page publique /g/{source} ================= */\n.orgpub-head { display: flex; gap: 20px; align-items: flex-start; margin-top: 8px; }\n.orgpub-logo { width: 84px; height: 84px; object-fit: contain; background: var(--surface); border: 1.5px solid var(--ink); border-radius: 18px; padding: 8px; flex: none; box-shadow: var(--shadow-flat); }\n.orgpub-id h1 { font-size: clamp(26px, 4.4vw, 40px); margin: 6px 0 8px; color: var(--navy); }\n.orgpub-tagline { font-size: 16px; color: var(--ink-2); margin: 0 0 12px; max-width: 640px; }\n.orgpub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.orgpub-meta a.stat-chip:hover { background: var(--blue-soft); }\n.orgpub-verified { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); font-weight: 600; }\n.orgpub-desc { max-width: 720px; color: var(--ink-2); margin: 18px 0 6px; white-space: pre-line; }\n.orgpub .results-head h2 { display: inline-flex; align-items: center; gap: 8px; }\n@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n\n/* ================= Éditeur de profil (photo, bio, réseaux) ================= */\n.profil-edit { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--line-strong); }\n/* profil géré au hub Groupe KA — lecture seule */\n.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }\n.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }\n.hub-profile a.stat-chip:hover { background: var(--blue-soft); }\n.hub-profile .btn-primary { display: inline-flex; align-items: center; gap: 7px; }\n.pe-avatar-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.pe-avatar {\n width: 84px; height: 84px; border-radius: var(--r-pill); object-fit: cover;\n border: 2.5px solid var(--orange); box-shadow: var(--shadow-flat);\n display: flex; align-items: center; justify-content: center;\n}\n.pe-avatar-empty { background: var(--orange-soft); color: var(--orange); font-family: var(--font-display); font-weight: 700; font-size: 34px; }\n.pe-avatar-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }\n.pe-avatar-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n.pe-hint { font-size: 11px; color: var(--ink-3); }\n.pe-socials-title { font-size: 15px; margin-top: 6px; }\n.pe-socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }\n.pe-social { display: flex !important; flex-direction: row !important; align-items: center; gap: 0 !important; }\n.pe-social-ico {\n display: flex; align-items: center; justify-content: center;\n width: 40px; align-self: stretch; flex: none;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-ctl) 0 0 var(--r-ctl);\n}\n.pe-social input { flex: 1; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; border-left: 0; }\n\n/* profil public — à propos + réseaux */\n.pub-about { max-width: 640px; margin-top: 22px; }\n.pub-bio { font-size: 15.5px; color: var(--ink); margin: 0 0 12px; white-space: pre-line; }\n.pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.pub-socials { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }\n.pub-social {\n display: flex; align-items: center; justify-content: center;\n width: 44px; height: 44px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: 14px; color: var(--navy);\n transition: all 0.12s ease;\n}\n.pub-social:hover { background: var(--navy); border-color: var(--navy); color: var(--white); transform: translateY(-2px); box-shadow: var(--shadow-off-soft); }\n\n/* -----------------------------------------------------------------------------\n Lou-Ka Maps (framework Ka Maps par Groupe Ka)\n Jetons du design system appliqués au chrome de la carte + sync liste↔carte.\n----------------------------------------------------------------------------- */\n.mapview .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n}\n.mapview { position: relative; overflow: hidden; }\n.mapview .ka-map { position: absolute; inset: 0; }\n\n/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en orange */\n.lmap3d {\n position: relative; height: 340px; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.lmap3d .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n position: absolute; inset: 0;\n}\n.lmap3d-skel {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.lmap3d-legende {\n position: absolute; left: 10px; bottom: 10px; z-index: 5;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 4px 10px; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n font-size: 10.5px; font-weight: 500; color: var(--ink-2);\n pointer-events: none;\n}\n.lmap3d-legende i {\n width: 10px; height: 10px; border-radius: 3px;\n background: var(--orange); border: 1px solid rgba(20, 24, 20, 0.25);\n}\n@media (max-width: 780px) { .lmap3d { height: 280px; } }\n.ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }\n.ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }\n.ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }\n\n/* carte de liste synchronisée avec la carte (survol / sélection) */\n.map-card { border-radius: var(--r-card); }\n.map-card-sel { outline: 2px solid var(--blue); outline-offset: 2px; }\n\n/* =============================================================================\n HARMONISATION « ÉDITORIAL SHARP » (Groupe KA) — couche finale posée après\n les styles historiques : géométrie 10/6 px, bordures encre 1,5 px, ombres\n décalées dures, accent orange Lou-Ka. La carte Ka Maps (Mapbox) conserve\n son propre thème marine/orange à l'intérieur du canevas.\n============================================================================= */\n\n/* — contrôles : plus de pilules (les chips/badges gardent --r-pill) — */\n.nav a, .login-btn, .cta, .mv-pop-cta, .f-search, .f-more, .pager-btn,\n.onglet, .view-toggle, .fab, .f-ctl, .pass-gestionnaire, .hbar-track,\n.q-bar-track, .pass-progress { border-radius: var(--r-ctl); }\n.seg button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); }\n.seg button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }\n\n/* — header : papier translucide + trait d'encre — */\n.header {\n background: rgba(245, 243, 238, 0.92);\n border-bottom: 1.5px solid var(--ink);\n}\n.nav a { font-family: var(--font-display); }\n.nav a:hover { color: var(--accent-deep); background: var(--accent-soft); }\n.nav a.active { background: var(--ink); color: var(--accent); }\n.brand .ka {\n background: var(--accent); color: var(--on-accent);\n border-radius: 6px; padding: 0 6px 2px; margin-left: 2px; transform: rotate(-2deg);\n}\n.header .gk-badge { margin-left: 4px; flex: none; }\n@media (max-width: 1080px) { .header .gk-badge { display: none; } }\n.mm-badge {\n display: flex; align-items: center; min-height: var(--touch);\n padding: 8px 10px 2px;\n}\n\n/* — ticker : encre + mono — */\n.ticker { font-family: var(--font-mono); border-bottom: 1.5px solid var(--ink); }\n\n/* — états sélectionnés : encre + accent (fini le bleu) — */\n.seg button.on, .onglet.on, .view-toggle button.on, .f-more.on {\n background: var(--ink); border-color: var(--ink); color: var(--accent);\n}\n.pager-btn.on { background: var(--ink); border-color: var(--ink); color: var(--accent); }\n\n/* — CTA / FAB : aplat accent, bordure encre, ombre décalée — */\n.cta, .mv-pop-cta, .fab {\n background: var(--accent); color: var(--on-accent);\n border: 1.5px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);\n font-family: var(--font-display); font-weight: 700;\n}\n.cta:hover, .mv-pop-cta:hover, .fab:hover { background: var(--accent-deep); }\n.cta:active, .fab:active { box-shadow: none; }\n.fab { box-shadow: 4px 4px 0 var(--ink), 0 10px 28px rgba(20, 24, 20, 0.25); }\n.login-btn {\n background: var(--ink); color: var(--accent);\n border: 1.5px solid var(--ink); box-shadow: none;\n font-family: var(--font-display); font-weight: 700;\n}\n.login-btn:hover { background: var(--accent-deep); color: var(--on-accent); }\n\n/* — cartes : levée nette diagonale (signature du groupe) — */\n.card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }\n.role-card:hover:not(:disabled) { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); border-color: var(--ink); }\n.badge { border: 1.5px solid var(--ink); border-radius: var(--r-ctl); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.badge.type { background: var(--accent); color: var(--on-accent); }\n.source-tag { font-family: var(--font-mono); }\n\n/* — barre d'onglets mobile : papier + encre, actif = accent orange — */\n@media (max-width: 760px) {\n .tabbar { background: rgba(245, 243, 238, 0.95); border-top: 1.5px solid var(--ink); }\n .tabbar a { font-family: var(--font-display); }\n .tabbar a.active { color: var(--accent); }\n}\n.mobile-menu { background: var(--paper); border-bottom: 1.5px solid var(--ink); }\n\n/* — carte de membre KA : encre plutôt que marine — */\n.pc { background: linear-gradient(135deg, #1d231d 0%, var(--ink) 55%, #0b0e0b 100%); }\n\n/* — micro-étiquettes en mono (système) — */\n.f-ctl > span, .f-group > label, .field label, .tile-k, .pg-label,\n.pc-label, .pc-id-label, .crumbs, .viz-sub { font-family: var(--font-mono); }\n\n/* — page Contact (design system) — */\n.contact-page { padding: 44px 0 90px; }\n.contact-page h1 { margin: 10px 0 8px; }\n.contact-page .lede { color: var(--ink-2); max-width: 640px; margin: 0 0 26px; }\n.contact-grid { display: grid; gap: 16px; grid-template-columns: 1fr; margin: 26px 0; }\n@media (min-width: 768px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }\n.contact-card { padding: 18px 20px; }\n.contact-card .klabel { display: block; margin-bottom: 6px; }\n.contact-card a.mail {\n font-family: var(--font-mono); font-weight: 700; font-size: 13.5px;\n color: var(--accent-deep); word-break: break-all;\n text-decoration: underline; text-underline-offset: 3px;\n display: inline-flex; align-items: center; min-height: 44px;\n}\n.contact-card p { margin: 6px 0 0; color: var(--ink-2); font-size: 13.5px; }\n.contact-note {\n border-left: 3px solid var(--accent); background: var(--surface);\n border-top: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);\n border-bottom: 1.5px solid var(--ink); border-radius: 0 var(--r-card) var(--r-card) 0;\n padding: 14px 18px; max-width: 720px; color: var(--ink-2); font-size: 14px;\n}\n.contact-note b { color: var(--ink); }\n.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.contact-sites a {\n display: inline-flex; align-items: center; min-height: 34px;\n padding: 4px 12px; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); font-family: var(--font-mono); font-size: 11px;\n font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;\n transition: transform 0.12s, box-shadow 0.12s;\n}\n.contact-sites a:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.contact-hub { margin-top: 26px; }\n\n/* — rangée de navigation locale au-dessus du footer commun — */\n.prefooter { border-top: 1.5px solid var(--ink); background: var(--surface-2); padding: 18px 0; }\n.prefooter .container {\n display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px;\n font-family: var(--font-mono); font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n}\n.prefooter a, .prefooter .flink {\n color: var(--ink-2); text-decoration: none; text-transform: uppercase;\n letter-spacing: 0.08em; font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;\n min-height: var(--touch); display: inline-flex; align-items: center;\n}\n.prefooter a:hover, .prefooter .flink:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }\n.prefooter .baseline { width: 100%; font-family: var(--font-body); font-weight: 400; font-size: 13px; color: var(--ink-2); text-transform: none; letter-spacing: 0; }\n.ka-footer { margin-top: 0; }\n@media (max-width: 760px) { .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } }\n\n/* ============ Juste valeur (fair value) — pastilles & analyse de prix ====== */\n.fv-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border-radius: var(--r-pill); padding: 6px 12px;\n font-size: 12.5px; font-weight: 700; border: 1.5px solid var(--ink);\n white-space: nowrap;\n}\n.fv-badge .fv-ico { font-size: 10px; line-height: 1; }\n.fv-badge .fv-pct { margin-left: 4px; }\n.fv-badge.fv-compact { padding: 2px 8px; font-size: 11px; gap: 4px; margin-left: 8px; vertical-align: 2px; }\n/* états (mêmes jetons que les badges marché existants — daltonien-compatible :\n l'icône ▼ ≈ ▲ et le libellé portent l'information, pas la couleur seule) */\n.fv-badge.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.35); color: var(--blue); }\n.fv-badge.deal-ok { background: var(--surface); color: var(--ink-2); }\n.fv-badge.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.4); color: var(--orange-deep); }\n\n.f-fairvalue .fv-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 12px; flex-wrap: wrap; margin-bottom: 6px;\n}\n.f-fairvalue .fv-value { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--navy); }\n.f-fairvalue .fv-value small { font-size: 12px; font-weight: 500; color: var(--ink-3); }\n.f-fairvalue .fv-range { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.fv-histo { display: block; width: 100%; height: auto; margin: 10px 0 4px; }\n.fv-histo-lbl { font: 700 10px Inter, system-ui, sans-serif; fill: var(--ink); }\n.fv-histo-lbl-fv { fill: var(--accent-deep, #cc5500); }\n.fv-histo-axis { font: 500 9px Inter, system-ui, sans-serif; fill: var(--ink-3); }\n.fv-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12.5px; color: var(--ink-2); margin: 6px 0 4px; }\n.fv-demo { display: flex; flex-wrap: wrap; gap: 8px; }\n.chip.chip-deal.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* =============================================================================\n MapSearch — recherche synchronisée liste ↔ carte (search/MapSearch.tsx)\n Une seule vérité partagée : compteur, tri, survols et sélections miroirs.\n============================================================================= */\n\n.ms { position: relative; }\n.ms-list { position: relative; scroll-behavior: smooth; }\n\n.ms-list-head {\n display: flex; align-items: center; justify-content: space-between;\n gap: 10px; flex-wrap: wrap; padding: 2px 2px 4px;\n}\n.ms-count { font-size: 13.5px; color: var(--ink-2); }\n.ms-count b { color: var(--ink); font-size: 15px; }\n.ms-nopos {\n margin-left: 8px; font-size: 11px; color: var(--ink-3);\n border: 1px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 8px; cursor: help;\n}\n.ms-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }\n.ms-sort select {\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); padding: 6px 8px; font-size: 12.5px;\n font-weight: 600; color: var(--ink);\n}\n\n.ms-pills { margin: 0 0 4px; }\n\n.ms-refit {\n align-self: flex-start; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--ink-2);\n cursor: pointer; box-shadow: var(--shadow-off-soft);\n}\n.ms-refit:hover { background: var(--navy); color: var(--white); border-color: var(--navy); }\n\n/* barre de progression fine — jamais d'écran qui clignote */\n.ms-progress {\n position: sticky; top: 0; z-index: 4; height: 3px; border-radius: 2px;\n background: linear-gradient(90deg, transparent, var(--accent), transparent);\n background-size: 200% 100%; animation: ms-progress 1s linear infinite;\n}\n@keyframes ms-progress { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n.ms-empty {\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-card);\n padding: 26px 18px; text-align: center; color: var(--ink-2);\n}\n.ms-empty .big { margin-bottom: 6px; color: var(--ink-3); }\n.ms-empty h3 { margin: 0 0 6px; font-size: 16px; color: var(--ink); }\n.ms-empty p { margin: 0 0 14px; font-size: 13px; }\n.ms-empty-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n/* miroirs survol / sélection venus de la carte */\n.map-card { transition: transform 0.15s ease, box-shadow 0.15s ease; }\n.map-card-hover {\n outline: 2px solid var(--accent); outline-offset: 2px;\n transform: translateY(-1px);\n}\n.map-card-pulse { animation: ms-pulse 0.9s ease 1; }\n@keyframes ms-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* indicateur discret : l'annonce survolée sur la carte est hors écran */\n.ms-hover-hint {\n position: sticky; z-index: 5; align-self: center;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 5px 12px;\n font-size: 11.5px; font-weight: 600; pointer-events: none;\n box-shadow: var(--shadow-off-soft);\n}\n.ms-hover-hint.haut { top: 8px; order: -1; }\n.ms-hover-hint.bas { bottom: 8px; }\n\n/* ---------------- carrousel mobile synchronisé aux marqueurs ------------- */\n.ms-carousel {\n position: absolute; left: 0; right: 0; bottom: 10px; z-index: 12;\n display: flex; gap: 10px; overflow-x: auto; padding: 4px 12px;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n.ms-carousel::-webkit-scrollbar { display: none; }\n.ms-car-item {\n flex: 0 0 78%; max-width: 340px; scroll-snap-align: center;\n display: grid; grid-template-columns: 108px 1fr; gap: 0;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); overflow: hidden;\n box-shadow: var(--shadow-off-soft); text-decoration: none; color: inherit;\n}\n.ms-car-item.sel { outline: 2.5px solid var(--accent); outline-offset: 1px; }\n.ms-car-item img, .ms-car-item .smart-img, .ms-car-item .card-fallback {\n width: 108px; height: 96px; object-fit: cover; display: block;\n}\n.ms-car-body { padding: 9px 12px; min-width: 0; }\n.ms-car-price { font-size: 14.5px; font-weight: 700; color: var(--ink); display: flex; gap: 6px; align-items: center; }\n.ms-car-title {\n font-size: 12px; color: var(--ink-2); margin-top: 2px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.ms-car-meta { font-size: 11px; color: var(--ink-3); margin-top: 3px; }\n.ms-car-more {\n flex: 0 0 auto; scroll-snap-align: center; align-self: stretch;\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n background: var(--navy); color: var(--white);\n font-size: 12.5px; font-weight: 600; padding: 0 18px; cursor: pointer;\n}\n\n/* mobile : carte plein cadre, liste masquée, carrousel par-dessus la carte */\n@media (max-width: 780px) {\n .ms { height: calc(100dvh - 190px); }\n .ms-list { display: none; }\n .ms .mapview { height: 100%; }\n .ka-search-area { top: 8px; }\n}\n@media (min-width: 781px) {\n .ms-carousel { display: none; }\n}\n\n/* mobile : réorganisation du chrome carte autour du carrousel */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 56px; left: 10px; bottom: auto; }\n .ms .ka-loading { bottom: 132px; }\n .ms .ka-empty { transform: translate(-50%, -70%); }\n}\n/* l'auto-défilement vers la carte laisse l'entête de résultats visible */\n.ms { scroll-margin-top: 108px; }\n\n/* mobile, vue carte : chrome sans chevauchement (case en haut, outils sous\n elle, FAB Filtres au-dessus du carrousel) */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 88px; left: 10px; bottom: auto; }\n .ms .ka-locate { top: 88px; right: 10px; }\n .fab-carte { bottom: 148px; }\n}\n\n/* =============================================================================\n KA Scores — pastilles, jauges circulaires, page méthodologie\n============================================================================= */\n.ka-badge {\n display: inline-flex; align-items: center; gap: 4px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 1px 8px 1px 3px; font-size: 11px; font-weight: 700;\n color: var(--ink); background: var(--surface); line-height: 1.5;\n vertical-align: middle;\n}\n.ka-badge-logo {\n background: var(--ink); color: var(--white); border-radius: 999px;\n font-size: 8.5px; font-weight: 800; padding: 1.5px 5px; letter-spacing: 0.4px;\n}\n.ka-badge.ka-haut { border-color: var(--green, #256d43); }\n.ka-badge.ka-haut .ka-badge-logo { background: var(--green, #256d43); }\n.ka-badge.ka-bon { border-color: var(--accent); }\n.ka-badge.ka-bon .ka-badge-logo { background: var(--accent); }\n.ka-badge.ka-moyen { opacity: 0.9; }\n.ka-badge.ka-bas { opacity: 0.75; }\n\n.ka-circles, .ka-demo-circles {\n display: flex; gap: 18px; flex-wrap: wrap; margin: 14px 0 6px;\n}\n.ka-circle { text-align: center; width: 92px; }\n.ka-circle-fond {\n fill: none; stroke: var(--line); stroke-width: 6;\n}\n.ka-circle-arc {\n fill: none; stroke: var(--ink-3); stroke-width: 6; stroke-linecap: round;\n transition: stroke-dasharray 0.6s ease;\n}\n.ka-circle.ka-haut .ka-circle-arc { stroke: var(--green, #256d43); }\n.ka-circle.ka-bon .ka-circle-arc { stroke: var(--accent); }\n.ka-circle.ka-moyen .ka-circle-arc { stroke: var(--orange, #d97a2b); }\n.ka-circle.ka-bas .ka-circle-arc { stroke: #b3543f; }\n.ka-circle-val { font: 700 17px var(--font-body); fill: var(--ink); }\n.ka-circle-nom { font-size: 12px; font-weight: 700; color: var(--ink); margin-top: 4px; }\n.ka-circle-label { font-size: 10.5px; color: var(--ink-3); }\n\n.f-kascores h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ka-perso {\n font-size: 12px; font-weight: 600; color: var(--ink-2);\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 10px;\n}\n.ka-detail summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n padding: 6px 0;\n}\n.ka-detail-grille {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px; padding: 8px 0 4px;\n}\n.ka-detail-grille h4 { margin: 0 0 4px; font-size: 12px; color: var(--ink); }\n.ka-detail-grille ul { margin: 0 0 10px; padding-left: 16px; font-size: 12.5px; color: var(--ink-2); }\n.ka-detail-grille li { margin: 2px 0; }\n\n.ka-poids { display: grid; gap: 10px; margin: 14px 0; max-width: 460px; }\n.ka-poids-row {\n display: grid; grid-template-columns: 90px 1fr 48px; align-items: center;\n gap: 12px; font-size: 13px; font-weight: 600; color: var(--ink);\n}\n.ka-poids-row input[type=\"range\"] { accent-color: var(--accent); }\n.ka-poids-row b { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ms-car-price .ka-badge { margin-left: 2px; }\n@media (max-width: 780px) {\n .ms .ka-3d { top: 148px; right: 10px; }\n}\n\n\n/* ---- Registre des loyers (fiche) — bloc citoyen registre-des-loyers.ca ---- */\n.f-rdl .rdl-resume { margin: 2px 0 6px; }\n.f-rdl .rdl-ecart { margin: 0 0 8px; }\n.rdl-rooms { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 10px; }\n.rdl-room { font-size: 13px; color: var(--ink-2); white-space: nowrap; }\n.rdl-room i { font-style: normal; color: var(--ink-3); }\n.rdl-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.rdl-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line, #e6e4df);\n vertical-align: top; }\n.rdl-table .rdl-addr { max-width: 46%; }\n.rdl-table .rdl-prix { font-weight: 600; white-space: nowrap; }\n.rdl-table .rdl-date { white-space: nowrap; color: var(--ink-2); }\n.rdl-table .rdl-dist { color: var(--ink-3); white-space: nowrap; text-align: right; }\n.rdl-room-on { background: var(--sand, #f4efe7); border-radius: 6px; padding: 2px 8px; }\n\n\n/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */\n.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }\n.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n font-size: 12.5px; font-weight: 700; }\n.zi-eleve { background: #fde8e8; color: #a12622; }\n.zi-modere { background: #fdf3e0; color: #8a5a00; }\n.zi-present { background: #fdf3e0; color: #8a5a00; }\n.zi-ok { background: #e7f4ea; color: #1e6b34; }\n.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n\n\n/* ---- Historique TAL (fiche) — décisions SOQUIJ à l'adresse ---- */\n.f-tal .tal-pending, .f-tal .tal-ok, .f-tal .tal-resume { margin: 2px 0 8px; }\n.tal-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.tal-liste li { padding: 8px 0; border-top: 1px solid var(--line, #e6e4df); }\n.tal-ligne { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }\n.tal-date { font-weight: 600; color: var(--ink-2); }\n.tal-part { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: var(--sand, #f0ede8); color: var(--ink-2); }\n.tal-part-locateur { background: #fdf3e0; color: #8a5a00; }\n.tal-verdict { font-size: 12px; color: var(--ink-3); }\n.tal-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 5px 0 3px; }\n.tal-tag { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: #fde8e8; color: #a12622; }\n.tal-ref { font-size: 12.5px; color: var(--ink-3); }\n\n\n/* ---- Registre des loyers v2 : tuiles, bande de position, barres ---- */\n.rdl-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 12px; }\n.rdl-kpi { flex: 1 1 90px; min-width: 90px; background: var(--surface-2, #f7f5f1);\n border-radius: 10px; padding: 10px 12px; }\n.rdl-kpi-v { display: block; font-size: 20px; font-weight: 700;\n letter-spacing: -0.02em; color: var(--navy); }\n.rdl-kpi-l { display: block; font-size: 11px; line-height: 1.35;\n color: var(--ink-3); margin-top: 2px; }\n.rdl-strip { width: 100%; height: auto; display: block; margin: 2px 0 4px; }\n.rdl-strip-track { fill: var(--surface-2, #f0ede8); }\n.rdl-strip-box { fill: var(--accent-soft, #fff1e6);\n stroke: var(--accent, #ff6a00); stroke-width: 1; }\n.rdl-strip-med { stroke: var(--ink-2, #4c4a45); stroke-width: 2; }\n.rdl-strip-me { stroke: var(--accent-deep, #cc5500); stroke-width: 2; }\n.rdl-strip-me-dot { fill: var(--accent-deep, #cc5500);\n stroke: var(--surface, #fff); stroke-width: 2; }\n.rdl-strip-lbl, .rdl-strip-axis { font-size: 10.5px; fill: var(--ink-3); }\n.rdl-strip-me-lbl { font-size: 11px; font-weight: 700;\n fill: var(--accent-deep, #cc5500); }\n.rdl-bars { width: 100%; height: auto; display: block; margin: 6px 0 2px; }\n.rdl-bar { fill: var(--accent, #ff6a00); opacity: 0.55; }\n.rdl-bar-on { fill: var(--accent-deep, #cc5500); opacity: 1; }\n.rdl-bar-val { font-size: 10.5px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat { font-size: 11px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat-on { font-weight: 700; fill: var(--ink, #1c1b18); }\n.rdl-bar-n { font-size: 9.5px; fill: var(--ink-3, #8a877f); }\n.rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }\n.rdl-ecart { margin: 4px 0 8px; }\n.rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }\n\n\n/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */\n.q-crime { width: 100%; }\n.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }\n.q-crime-cats li { display: flex; align-items: center; gap: 8px;\n padding: 3px 0; font-size: 12.5px; }\n.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.q-crime-barre i { display: block; height: 100%; border-radius: 5px;\n background: var(--accent, #ff6a00); opacity: 0.6; }\n.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; }\n.q-crime-n small { font-weight: 400; color: var(--ink-3); }\n.q-crime-src { margin-top: 8px; }\n\n\n/* ---- Qualité de l'air (RSQAQ) + Essence à proximité ---- */\n.air-liste { list-style: none; margin: 10px 0 0; padding: 0; }\n.air-liste li { display: flex; align-items: center; gap: 8px;\n padding: 4px 0; font-size: 12.5px; }\n.air-nom { flex: 0 0 44%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.air-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.air-barre i { display: block; height: 100%; border-radius: 5px;\n background: #4d9e64; }\n.air-barre i.air-sur { background: #c96a1f; }\n.air-val { flex: 0 0 34%; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.air-val small { font-weight: 400; color: var(--ink-3); }\n.air-val .air-ref { display: block; font-size: 10px; }\n.gaz-head th { text-align: left; font-size: 11px; color: var(--ink-3);\n font-weight: 600; padding: 2px 8px 2px 0; }\n.gaz-best { display: inline-block; margin-left: 6px; padding: 1px 7px;\n border-radius: 999px; background: #e7f4ea; color: #1e6b34;\n font-size: 10.5px; font-weight: 700; }\n.gaz-adr { display: block; font-size: 11px; color: var(--ink-3); }\n\n\n/* ---- Commerces et transport (fiche) ---- */\n.cm-grille { list-style: none; margin: 6px 0 0; padding: 0;\n display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }\n@media (max-width: 560px) { .cm-grille { grid-template-columns: 1fr; } }\n.cm-item { display: flex; align-items: center; gap: 9px; padding: 5px 0;\n border-bottom: 1px solid var(--line, #eeece7); min-width: 0; }\n.cm-ico { flex: 0 0 28px; }\n.cm-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.cm-nom { font-size: 13px; font-weight: 600; color: var(--ink, #222);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-poi { font-size: 11px; color: var(--ink-3, #8a877f);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-dist { flex: 0 0 auto; font-size: 13px; font-weight: 700;\n color: var(--ink-2, #4c4a45); white-space: nowrap; }\n\n\n/* ---- Coût d'électricité (Hydro-Québec) ---- */\n.hydro-intro { margin: 2px 0 12px; color: var(--ink-2, #4c4a45); }\n.hydro-btn { appearance: none; border: 0; border-radius: 10px; cursor: pointer;\n background: var(--accent, #2b7a3b); color: #fff; font-weight: 700;\n font-size: 14px; padding: 11px 18px; }\n.hydro-btn:disabled { opacity: 0.6; cursor: progress; }\n.hydro-montant { font-size: 26px; font-weight: 800; letter-spacing: -0.02em;\n color: var(--navy, #1c1b18); display: flex; align-items: baseline;\n gap: 10px; flex-wrap: wrap; }\n.hydro-montant small { font-size: 14px; font-weight: 500; color: var(--ink-3); }\n.hydro-an { font-size: 14px; font-weight: 600; color: var(--ink-2, #4c4a45); }\n.hydro-vide { color: var(--ink-3, #8a877f); }\n\n\n/* ============================================================================\n PASSE DE DESIGN v3 — « éditorial sharp » appliqué à toute la fiche\n Blocs = cartes encre (bordure 1,5 px + ombre décalée douce), titres à\n marqueur accent + filet, tuiles KPI blanches à ombre dure, tableaux zébrés,\n barres en dégradé, pastilles unifiées (mono, point d'état, liseré).\n Aucune propriété `order` (règle Groupe Ka : ordre DOM = ordre visuel).\n ========================================================================== */\n\n/* --- Blocs de la fiche : cartes posées sur le papier --- */\n.f-bloc:not(.f-galerie):not(.f-hero) {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-card);\n padding: 18px 20px 16px;\n box-shadow: var(--shadow-off-soft);\n}\n@media (max-width: 640px) {\n .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n}\n\n/* --- Titres de section : carré accent + filet éditorial --- */\n.f-bloc h2, .quartier h2 {\n display: flex; align-items: center; gap: 10px;\n font-size: 19px; letter-spacing: -0.015em; margin-bottom: 14px;\n}\n.f-bloc h2::before, .quartier h2::before {\n content: \"\"; flex: 0 0 9px; width: 9px; height: 9px;\n background: var(--accent); border: 1.5px solid var(--ink);\n box-shadow: 2px 2px 0 var(--ink);\n}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }\n.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--ink);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 22px;\n color: var(--accent-deep, #cc5500);\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 30px;\n line-height: 1; color: var(--ink);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 8px 12px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n","structuredPatch":[{"oldStart":2211,"oldLines":10,"newStart":2211,"newLines":14,"lines":[" .gest-head { margin-bottom: 10px; }"," .gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }"," .gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }","-.gest-google { display: flex; align-items: baseline; gap: 10px; margin: 8px 0; }","+.gest-google { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }"," .gest-note {","- font-family: var(--font-display); font-weight: 700; font-size: 22px;","+ font-family: var(--font-display); font-weight: 700; font-size: 20px;"," color: var(--accent-deep, #cc5500);","+ background: var(--surface); border: 1.5px solid var(--ink);","+ border-radius: var(--r-ctl); padding: 5px 10px;","+ box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);","+ white-space: nowrap;"," }"," .gest-navis { font-size: 12.5px; color: var(--ink-2); }"," .gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMex7jLfxPUhaBiRB9i","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014Vez78SAbUmsEkdfDnng2Z","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","old_string":".hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}","new_string":".hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n\n/* --- Adaptatif mobile : dossier d'intelligence locative --- */\n@media (max-width: 640px) {\n .cr-table { font-size: 12.5px; }\n .cr-table td { padding: 6px 6px 6px 0; }\n .cr-poste .st-pill { margin-left: 6px; }\n .cr-montant { font-size: 12.5px; }\n .hl-date { display: block; min-width: 0; margin-bottom: 2px; }\n .hl-item { font-size: 12.5px; }\n .hl-match { font-size: 12.5px; }\n .im-indic .im-detail { display: block; margin-top: 3px; }\n .gest-nom { font-size: 16px; }\n .gest-stats { font-size: 12.5px; gap: 8px; }\n .gest-citation { font-size: 12.5px; }\n .gb-n { min-width: 24px; font-size: 10.5px; }\n .gb-c { min-width: 20px; font-size: 10.5px; }\n .hiver-score { font-size: 26px; padding: 6px 10px; }\n .hiver-sur { flex-basis: 100%; }\n .hd-crit { min-width: 86px; font-size: 10px; }\n .hiver-detail li { font-size: 12.5px; }\n}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":2670,"cache_read_input_tokens":107477,"cache_creation":{"ephemeral_5m_input_tokens":2670,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b266e5fb-e7af-40d8-adef-4489bdc09609"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014Vez78SAbUmsEkdfDnng2Z","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7c506e50-d1b1-40a4-b430-5adc87fdc094","timestamp":"2026-08-24T22:24:30.542Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/frontend/src/styles.css","oldString":".hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}","newString":".hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n\n/* --- Adaptatif mobile : dossier d'intelligence locative --- */\n@media (max-width: 640px) {\n .cr-table { font-size: 12.5px; }\n .cr-table td { padding: 6px 6px 6px 0; }\n .cr-poste .st-pill { margin-left: 6px; }\n .cr-montant { font-size: 12.5px; }\n .hl-date { display: block; min-width: 0; margin-bottom: 2px; }\n .hl-item { font-size: 12.5px; }\n .hl-match { font-size: 12.5px; }\n .im-indic .im-detail { display: block; margin-top: 3px; }\n .gest-nom { font-size: 16px; }\n .gest-stats { font-size: 12.5px; gap: 8px; }\n .gest-citation { font-size: 12.5px; }\n .gb-n { min-width: 24px; font-size: 10.5px; }\n .gb-c { min-width: 20px; font-size: 10.5px; }\n .hiver-score { font-size: 26px; padding: 6px 10px; }\n .hiver-sur { flex-basis: 100%; }\n .hd-crit { min-width: 86px; font-size: 10px; }\n .hiver-detail li { font-size: 12.5px; }\n}","originalFile":"/* -----------------------------------------------------------------------------\n Lou-Ka — Agrégateur de logements à louer (province de Québec)\n Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n styles.css : styles LOCAUX de Lou-Ka, posés SUR le design system commun\n Groupe KA « éditorial sharp » (frontend/src/ka/tokens.css, importé AVANT\n ce fichier dans main.tsx).\n · Palette commune : papier #f5f3ee · encre #141814 · vert profond #1c5c41\n · ACCENT Lou-Ka : orange #FF6A00 (seule surcharge de tokens.css)\n · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono\n · Signature : bordures encre 1,5 px + ombres décalées dures, rayons 10/6 px\n · Baseline : « La porte d'entrée vers votre prochain chez-vous. »\n · 100 % adaptatif mobile (PWA installable, safe-areas iOS)\n----------------------------------------------------------------------------- */\n:root {\n /* ---- ACCENT Lou-Ka (surcharge officielle des jetons ka-ui) ---- */\n --accent: #ff6a00;\n --accent-soft: #fff1e6;\n --accent-deep: #cc5500;\n --on-accent: #ffffff;\n --lime: var(--accent);\n --lime-soft: var(--accent-soft);\n\n /* ---- Alias hérités de l'ancienne identité marine/bleu — remappés sur\n les jetons communs (le fond redevient papier via tokens.css) ---- */\n --navy: var(--ink);\n --navy-soft: var(--surface-2);\n --blue: var(--green);\n --blue-soft: #e8f0ea;\n --orange: var(--accent);\n --orange-soft: var(--accent-soft);\n --orange-deep: var(--accent-deep);\n --grey: var(--surface-2);\n --white: #ffffff;\n}\n\n#root { position: relative; z-index: 1; }\na { text-decoration: none; }\nbutton { font-family: inherit; }\n\n.mono { font-family: var(--font-mono); }\n\n/* Garde-fou : jamais de défilement horizontal de la page, même transitoire\n pendant le chargement (bandeau défilant, polices) — clip ne casse pas\n position: sticky, contrairement à hidden. */\nhtml, body { overflow-x: clip; }\n\n/* ================= Header ================= */\n.header {\n position: sticky; top: 0; z-index: var(--z-header, 500);\n background: var(--paper);\n border-bottom: 1px solid var(--line);\n}\n.header-inner { display: flex; align-items: center; gap: 20px; height: 64px; }\n.brand {\n font-family: var(--font-display); font-weight: 700; font-size: 25px;\n letter-spacing: -0.03em; display: flex; align-items: center; line-height: 1;\n color: var(--navy);\n}\n.brand .logo-ico { margin-right: 12px; flex: none; } /* ≈ 40 % de la largeur de l'icône */\n.brand .ka { color: var(--orange); }\n.brand-tag {\n font-size: 11px; font-weight: 500; color: var(--ink-3);\n letter-spacing: 0.02em; margin-left: 12px;\n}\n@media (max-width: 900px) { .brand-tag { display: none; } }\n.nav { margin-left: auto; display: flex; gap: 4px; }\n.nav a {\n padding: 9px 16px; border-radius: var(--r-pill); font-weight: 600; font-size: 14px;\n color: var(--navy); transition: all 0.15s ease; min-height: 40px;\n display: inline-flex; align-items: center;\n}\n.nav a:hover { color: var(--orange); background: var(--orange-soft); }\n.nav a.active { background: var(--navy); color: var(--white); }\n\n/* ================= Ticker ================= */\n.ticker {\n background: var(--navy); color: rgba(255, 255, 255, 0.85); overflow: hidden;\n font-size: 11.5px; font-weight: 500; letter-spacing: 0.08em;\n text-transform: uppercase; padding: 7px 0; white-space: nowrap;\n}\n.ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }\n.ticker span { padding: 0 26px; position: relative; }\n.ticker span::after { content: \"◆\"; position: absolute; right: -6px; color: var(--orange); opacity: 0.8; font-size: 8px; top: 3px; }\n@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) {\n .ticker-track { animation: none; }\n * { transition-duration: 0.01ms !important; }\n}\n\n/* ================= Hero ================= */\n.hero { padding: 58px 0 22px; }\n@media (max-width: 640px) { .hero { padding: 34px 0 14px; } }\n.hero h1 {\n font-size: clamp(36px, 5.4vw, 56px); font-weight: 700; line-height: 1.06;\n letter-spacing: -0.03em; max-width: 900px; margin-top: 14px; color: var(--navy);\n}\n.hero h1 .outline { color: var(--navy); }\n.hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; }\n.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }\n.stat-chip {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 8px 16px; font-size: 12.5px; font-weight: 500;\n color: var(--ink-2); display: flex; gap: 8px; align-items: center;\n box-shadow: var(--shadow-flat);\n}\n.stat-chip b { color: var(--navy); font-weight: 700; }\n.stat-chip .pulse {\n width: 8px; height: 8px; border-radius: 50%; background: var(--orange);\n box-shadow: 0 0 0 4px var(--orange-soft); animation: pulse 2.4s ease infinite;\n}\n@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 106, 0, 0.18); } }\n\n/* ================= Rangée d'avantages ================= */\n.benefits {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px; margin-top: 30px;\n}\n.benefit { display: flex; align-items: center; gap: 12px; }\n.benefit-ico {\n width: 42px; height: 42px; border-radius: var(--r-pill); flex: none;\n display: flex; align-items: center; justify-content: center;\n background: var(--orange-soft); color: var(--orange);\n}\n.benefit-ico.blue { background: var(--blue-soft); color: var(--blue); }\n.benefit-label {\n font-size: 11.5px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--navy);\n}\n@media (max-width: 640px) {\n .benefits { grid-template-columns: 1fr 1fr; gap: 12px; }\n}\n\n/* ================= Filter bar ================= */\n.filterbar {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 14px; margin: 30px 0 6px;\n display: flex; flex-direction: column; gap: 0; min-width: 0;\n}\n\n/* — rangée principale — */\n.f-primary { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; min-width: 0; }\n.f-search {\n flex: 1 1 220px; min-width: 0; max-width: 440px; align-self: center;\n display: flex; align-items: center; gap: 8px;\n border: 1px solid transparent; border-radius: var(--r-pill); background: var(--grey);\n padding: 0 15px; min-height: 42px; color: var(--ink-2);\n transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;\n}\n.f-search:focus-within {\n background: var(--white); border-color: var(--blue);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n.f-search input {\n border: none; background: none; outline: none; flex: 1; min-width: 0;\n font-size: 14px; color: var(--ink); font-family: inherit;\n}\n.f-clear {\n border: none; background: var(--line); color: var(--ink-2); border-radius: 50%;\n width: 20px; height: 20px; font-size: 10px; cursor: pointer; flex: none;\n display: grid; place-items: center;\n}\n.f-ctl {\n flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center;\n gap: 2px; border: 1.5px solid var(--ink); border-radius: 14px; background: var(--surface);\n padding: 7px 14px 6px; min-height: 52px; cursor: pointer;\n transition: box-shadow 0.15s ease, border-color 0.15s ease;\n}\n.f-ctl:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.f-ctl > span {\n font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-ctl select {\n border: none; background: transparent; outline: none; font-family: var(--font-display);\n font-weight: 700; font-size: 14.5px; color: var(--navy); cursor: pointer;\n appearance: none; -webkit-appearance: none; padding-right: 16px; min-width: 0; max-width: 170px;\n text-overflow: ellipsis;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right center;\n}\n.range-pair { display: flex; align-items: center; gap: 4px; }\n.range-pair select { max-width: 86px; }\n.range-sep { color: var(--ink-3); font-size: 11px; }\n.f-more {\n flex: none; align-self: stretch; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); color: var(--navy); padding: 0 20px; cursor: pointer;\n font-family: var(--font-display); font-weight: 600; font-size: 14px; min-height: 52px;\n transition: all 0.13s ease;\n}\n.f-more:hover { background: var(--blue-soft); border-color: var(--blue); color: var(--blue); }\n.f-more.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* — panneau avancé — */\n.f-adv {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 22px;\n border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; min-width: 0;\n animation: adv-in 0.18s ease;\n}\n@keyframes adv-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }\n.f-group { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.f-group > label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);\n}\n.f-group-end { justify-content: flex-end; }\n.f-group .btn:disabled { opacity: 0.4; cursor: default; }\n.f-native {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 10px 30px 10px 14px; font-size: 14px; color: var(--ink); outline: none;\n font-family: inherit; min-height: 42px; width: 100%; min-width: 0;\n appearance: none; -webkit-appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center;\n}\n.f-native:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); background: var(--white); }\n\n/* segments (pilules soudées) */\n.seg { display: inline-flex; flex-wrap: wrap; row-gap: 6px; }\n.seg button {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink-2);\n padding: 8px 14px; font-family: var(--font-display); font-weight: 600; font-size: 13px;\n cursor: pointer; margin-left: -1px; white-space: nowrap; min-height: 38px;\n transition: all 0.12s ease;\n}\n.seg button:first-child { border-radius: var(--r-pill) 0 0 var(--r-pill); margin-left: 0; padding-left: 16px; }\n.seg button:last-child { border-radius: 0 var(--r-pill) var(--r-pill) 0; padding-right: 16px; }\n.seg button:hover { background: var(--blue-soft); color: var(--blue); }\n.seg button.on { background: var(--blue); border-color: var(--blue); color: var(--white); position: relative; z-index: 1; }\n\n/* pastilles de filtres actifs */\n.pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }\n.pill {\n display: inline-flex; align-items: center; gap: 7px;\n border: 1px solid rgba(28, 92, 65, 0.3); background: var(--blue-soft); color: var(--blue);\n border-radius: var(--r-pill); padding: 6px 14px; font-size: 12.5px; font-weight: 600;\n cursor: pointer; transition: all 0.13s ease;\n}\n.pill:hover { background: var(--blue); color: var(--white); }\n.pill-x { font-size: 10px; opacity: 0.65; }\n.pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }\n.pill-clear:hover { background: var(--danger); color: #fff; }\n.chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 4px 4px; flex: none; }\n.link-btn {\n background: none; border: none; padding: 0; color: var(--blue); font: inherit;\n text-decoration: underline; cursor: pointer;\n}\n.field { display: flex; flex-direction: column; gap: 5px; }\n.field label {\n font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); padding-left: 2px;\n}\n.field input, .field select {\n border: 1.5px solid var(--ink); background: var(--grey); border-radius: var(--r-ctl);\n padding: 11px 14px; font-size: 15px; color: var(--ink); outline: none; font-family: inherit;\n transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; min-height: 44px;\n appearance: none; -webkit-appearance: none;\n}\n.field select {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px;\n}\n.field input:focus, .field select:focus {\n border-color: var(--blue); background: var(--white);\n box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15);\n}\n/* .btn / .btn-primary / .btn-ghost : fournis par tokens.css (ka-ui). */\n\n/* ================= Chips ================= */\n.chips { display: flex; gap: 8px; margin: 16px 0 4px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }\n.chips::-webkit-scrollbar { display: none; }\n.chip {\n border: 1.5px solid var(--ink); background: var(--surface); color: var(--ink);\n border-radius: var(--r-pill); padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer;\n font-family: var(--font-display); white-space: nowrap; min-height: 40px;\n transition: transform 0.13s, box-shadow 0.13s, background 0.13s, color 0.13s;\n}\n.chip:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.chip.on { background: var(--ink); border-color: var(--ink); color: var(--accent); box-shadow: 3px 3px 0 var(--accent); }\n\n/* ================= Results ================= */\n.results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }\n.results-head h2 { font-size: 22px; letter-spacing: -0.02em; color: var(--navy); }\n.results-head span { color: var(--ink-3); font-size: 12.5px; font-weight: 500; }\n.grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));\n gap: 22px; padding-bottom: 70px;\n}\n@media (max-width: 640px) { .grid { grid-template-columns: 1fr; gap: 16px; padding-bottom: 48px; } }\n\n.card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-flat);\n transition: transform 0.16s ease, box-shadow 0.16s ease;\n}\n.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-off); }\n.card:focus-visible { outline: 3px solid rgba(28, 92, 65, 0.4); outline-offset: 2px; }\n.card-img { position: relative; aspect-ratio: 16/10.5; background: var(--grey); overflow: hidden; }\n.card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }\n.card:hover .card-img img { transform: scale(1.05); }\n.card-img .noimg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--ink-3); font-size: 34px; }\n.badge {\n position: absolute; top: 12px; left: 12px; border-radius: var(--r-pill); padding: 4px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: 0.04em;\n background: rgba(255, 255, 255, 0.94); color: var(--navy);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.badge.type { background: var(--orange); color: var(--white); }\n.badge.right { left: auto; right: 12px; background: rgba(255, 255, 255, 0.92); }\n.card-body { padding: 16px 18px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\n.card-price { font-family: var(--font-display); font-weight: 700; font-size: 21px; letter-spacing: -0.02em; color: var(--navy); }\n.card-price small { font-weight: 500; color: var(--ink-3); font-size: 11.5px; }\n.card-title { font-weight: 600; font-size: 14.5px; color: var(--ink); }\n.card-meta { color: var(--ink-3); font-size: 12.5px; display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }\n.card-meta .sep { width: 4px; height: 4px; background: var(--orange); border-radius: 50%; }\n.card-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.source-tag {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--blue); background: var(--blue-soft);\n border-radius: var(--r-pill); padding: 3px 10px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;\n}\n.avail { font-size: 11.5px; color: var(--ink-2); font-weight: 500; text-align: right; }\n\n/* ================= Skeletons ================= */\n@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }\n.skel { border-radius: var(--r-card); border: 1.5px solid var(--ink); overflow: hidden; background: var(--surface); }\n.skel .sk-img, .skel .sk-line {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.skel .sk-img { aspect-ratio: 16/10.5; }\n.skel .sk-line { height: 14px; border-radius: 4px; margin: 12px 16px; }\n.skel .sk-line.short { width: 45%; }\n\n/* ================= Empty / error ================= */\n.notice { text-align: center; padding: 72px 24px; color: var(--ink-2); }\n.notice .big { font-size: 44px; margin-bottom: 10px; }\n.notice h2 { color: var(--navy); }\n\n/* ================= Detail page ================= */\n.detail { padding: 30px 0 90px; }\n.crumbs {\n font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;\n color: var(--ink-3); margin-bottom: 20px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;\n}\n.crumbs a { border-bottom: 1.5px solid transparent; }\n.crumbs a:hover { color: var(--blue); border-color: var(--blue); }\n.detail-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }\n/* les enfants de grille ont min-width:auto par défaut : un contenu large\n (grille du quartier) déborderait de l'écran mobile sans ceci */\n.detail-grid > * { min-width: 0; }\n\n.gallery { display: flex; flex-direction: column; gap: 10px; }\n.gallery-main {\n border-radius: var(--r-card); overflow: hidden; border: 1.5px solid var(--ink);\n aspect-ratio: 16/10; background: var(--grey); cursor: zoom-in; box-shadow: var(--shadow-off-soft);\n}\n.gallery-main img { width: 100%; height: 100%; object-fit: cover; }\n.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; }\n.thumbs button {\n border: 2px solid var(--line); border-radius: 12px; overflow: hidden; padding: 0; cursor: pointer;\n aspect-ratio: 4/3; background: var(--grey); transition: border-color 0.12s ease, transform 0.12s ease;\n}\n.thumbs button:hover { transform: translateY(-2px); }\n.thumbs button.on { border-color: var(--orange); }\n.thumbs img { width: 100%; height: 100%; object-fit: cover; }\n\n.panel {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px; position: sticky; top: 120px;\n}\n@media (max-width: 900px) { .panel { position: static; } }\n.panel .price { font-family: var(--font-display); font-size: 34px; font-weight: 700; letter-spacing: -0.03em; color: var(--navy); }\n.panel .price small { font-size: 12.5px; color: var(--ink-3); font-weight: 500; }\n.panel h1 { font-size: 22px; margin: 8px 0 2px; }\n.panel .loc { color: var(--ink-2); font-size: 14px; }\n.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }\n@media (max-width: 380px) { .kv { grid-template-columns: 1fr; } }\n.kv .cell { background: var(--grey); border-radius: var(--r-ctl); padding: 10px 14px; }\n.kv .cell .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }\n.kv .cell .v { font-weight: 700; font-size: 14.5px; margin-top: 2px; font-family: var(--font-display); color: var(--navy); }\n/* .klabel : fourni par tokens.css (ka-ui). */\n.panel .klabel, .f-bloc .klabel { display: block; margin-bottom: 8px; }\n.amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }\n.amenity {\n background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 600;\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-pill); padding: 5px 13px;\n}\n.cta {\n display: block; text-align: center; background: var(--orange); color: var(--white);\n font-weight: 600; font-family: var(--font-display); border-radius: var(--r-pill);\n padding: 15px; min-height: 48px;\n box-shadow: 0 8px 22px rgba(255, 106, 0, 0.34); transition: all 0.14s ease;\n}\n.cta:hover { background: var(--orange-deep); }\n.cta:active { transform: translateY(1px); }\n.panel .fine { font-size: 11.5px; color: var(--ink-3); margin-top: 14px; text-align: center; }\n\n/* ================= Lightbox ================= */\n.lightbox {\n position: fixed; inset: 0; background: rgba(20, 24, 20, 0.94); z-index: var(--z-modal, 900);\n display: flex; align-items: center; justify-content: center; cursor: zoom-out;\n padding: max(16px, env(safe-area-inset-top)) 16px;\n}\n.lightbox img { max-width: 94vw; max-height: 90vh; border-radius: 12px; }\n\n/* Lightbox v2 : balayage entre photos (scroll-snap) + pincement pour zoomer */\n.lightbox-v2 { padding: 0; cursor: default; display: block; }\n.lightbox-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%;\n -webkit-overflow-scrolling: touch; scrollbar-width: none;\n touch-action: pan-x pinch-zoom;\n}\n.lightbox-track::-webkit-scrollbar { display: none; }\n.lightbox-cell {\n flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center;\n display: flex; align-items: center; justify-content: center;\n padding: max(52px, env(safe-area-inset-top)) 10px max(24px, env(safe-area-inset-bottom));\n overflow: hidden;\n}\n.lightbox-cell img {\n max-width: 100%; max-height: 100%; border-radius: 10px;\n user-select: none; -webkit-user-drag: none; will-change: transform;\n}\n.lightbox-close {\n position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.4);\n background: rgba(20, 24, 20, 0.6); color: #fff; font-size: 18px; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.lightbox-count {\n position: fixed; left: 50%; transform: translateX(-50%); right: auto;\n top: max(22px, env(safe-area-inset-top)); bottom: auto; z-index: 3;\n}\n.lightbox-v2 .carousel-nav { position: fixed; z-index: 3; }\n\n/* visuel de secours (SmartImg) directement sous .carousel (galerie vide) */\n.carousel > img { display: block; width: 100%; aspect-ratio: 16/11; object-fit: cover; }\n\n/* ================= Sources page ================= */\n.sources { padding: 44px 0 90px; }\n.sources h1 { font-size: clamp(28px, 4vw, 40px); margin: 10px 0 6px; color: var(--navy); }\n.sources .sub { color: var(--ink-2); margin-bottom: 30px; max-width: 700px; }\n.src-wrap { overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); background: var(--surface); }\n.src-table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 720px; }\n.src-table th {\n text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: 0.1em; color: var(--ink-3); padding: 14px 18px;\n border-bottom: 1px solid var(--line-strong); background: var(--grey);\n}\n.src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }\n.src-table tr:last-child td { border-bottom: none; }\n.src-table tr:hover td { background: var(--navy-soft); }\n.src-table a { color: var(--blue); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.pill { display: inline-block; border-radius: var(--r-pill); padding: 3px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }\n.pill.ok { background: var(--blue-soft); color: var(--blue); }\n.pill.todo { background: var(--orange-soft); color: var(--orange-deep); }\n.count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--navy); }\n\n/* ================= Page Statistiques ================= */\n.stats-page { padding: 44px 0 90px; }\n.stats-title { font-size: clamp(30px, 4.6vw, 46px); margin: 10px 0 6px; color: var(--navy); }\n.stats-page .sub { color: var(--ink-2); max-width: 640px; margin-bottom: 30px; }\n\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 30px; }\n.tile {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n padding: 18px 20px; box-shadow: var(--shadow-flat);\n}\n.tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--navy); }\n.tile-k { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }\n.hero-tile { background: var(--navy); color: var(--white); border-color: var(--navy); }\n.hero-tile .tile-v { color: var(--white); }\n.hero-tile .tile-k { color: rgba(255, 255, 255, 0.6); }\n\n.viz-card {\n background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off-soft); padding: 26px 28px 20px; margin-bottom: 22px;\n}\n.viz-card h2 { font-size: 19px; letter-spacing: -0.01em; color: var(--navy); }\n.viz-sub { font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 4px 0 20px; }\n.viz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }\n\n/* Histogramme (mono-série, bleu vif) */\n.histo { display: flex; align-items: stretch; gap: 2px; height: 240px; }\n.histo-col { flex: 1; display: flex; flex-direction: column; min-width: 0; cursor: default; }\n.histo-bar-zone { flex: 1; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line-strong); }\n.histo-bar {\n width: 100%; background: var(--blue); border-radius: 6px 6px 0 0;\n min-height: 2px; transition: background 0.12s ease;\n}\n.histo-col:hover .histo-bar { background: var(--orange); }\n.histo-x { font-size: 9.5px; color: var(--ink-3); text-align: left; height: 18px; padding-top: 5px; overflow: visible; white-space: nowrap; }\n\n/* Barres horizontales (mono-série) */\n.hbars { display: flex; flex-direction: column; gap: 7px; }\n.hbar-row {\n display: grid; grid-template-columns: minmax(96px, 170px) 1fr auto;\n gap: 12px; align-items: center; min-height: 26px; cursor: default;\n}\n.hbar-label { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.hbar-label a { border-bottom: 1.5px solid rgba(28, 92, 65, 0.35); }\n.hbar-label a:hover { color: var(--blue); }\n.hbar-track { background: var(--grey); border-radius: var(--r-pill); height: 18px; overflow: hidden; }\n.hbar-fill {\n display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill);\n min-width: 2px; transition: background 0.12s ease;\n}\n.hbar-row:hover .hbar-fill { background: var(--orange); }\n.hbar-value { font-size: 11.5px; font-weight: 700; white-space: nowrap; color: var(--navy); }\n.hbar-value em { font-style: normal; font-weight: 500; color: var(--ink-3); }\n@media (max-width: 640px) {\n .hbar-row { grid-template-columns: minmax(80px, 110px) 1fr auto; gap: 8px; }\n .hbar-value em { display: none; }\n .viz-card { padding: 20px 16px 14px; }\n .histo { height: 170px; }\n}\n\n/* Infobulle */\n.viz-tip {\n position: fixed; z-index: var(--z-toast, 950); pointer-events: none; max-width: 180px;\n background: var(--navy); color: var(--white); border-radius: 12px;\n padding: 9px 12px; font-size: 12px; line-height: 1.5;\n box-shadow: 0 8px 24px rgba(20, 24, 20, 0.35);\n}\n.viz-tip-title { font-family: var(--font-display); font-weight: 700; color: var(--orange); margin-bottom: 2px; }\n\n/* Vue tableau repliable (accessibilité / relief) */\n.viz-table { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 10px; }\n.viz-table summary {\n cursor: pointer; font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em; color: var(--blue);\n}\n.viz-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }\n.viz-table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line-strong); }\n.viz-table td { padding: 6px 10px; border-bottom: 1px solid var(--line); }\n\n.stats-foot { font-size: 11.5px; color: var(--ink-3); letter-spacing: 0.02em; margin-top: 6px; }\n\n/* ================= Mobile : feuille de filtres + FAB ================= */\n.sheet-head { display: none; }\n.sheet-apply { display: none; }\n.sheet-backdrop { display: none; }\n.fab { display: none; }\n\n@media (max-width: 640px) {\n /* En-tête compact */\n .header-inner { height: 56px; }\n .brand { font-size: 21px; }\n .nav a { padding: 8px 13px; font-size: 13.5px; }\n .ticker { font-size: 10.5px; padding: 6px 0; }\n\n /* Héro resserré + stats en rangée défilante */\n .hero h1 { font-size: clamp(30px, 8.6vw, 42px); }\n .hero p.lede { font-size: 15px; }\n .stat-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 6px; margin-right: -16px; padding-right: 16px; }\n .stat-row::-webkit-scrollbar { display: none; }\n .stat-chip { flex: 0 0 auto; white-space: nowrap; }\n\n /* La barre de filtres devient une feuille coulissante (bottom sheet) */\n .filterbar { display: none; }\n .filterbar.open .f-primary { flex-direction: column; }\n .filterbar.open .f-search { align-self: stretch; max-width: none; min-height: 46px; }\n .filterbar.open .f-search input { font-size: 16px; } /* anti-zoom iOS */\n .filterbar.open .f-ctl select { max-width: none; width: 100%; }\n .filterbar.open .f-more { display: none; }\n .filterbar.open .f-adv { margin-top: 4px; }\n .filterbar.open {\n display: flex; flex-direction: column; gap: 12px;\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);\n margin: 0; border-radius: 24px 24px 0 0; border: none;\n max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;\n padding: 16px 18px calc(18px + env(safe-area-inset-bottom));\n box-shadow: 0 -16px 48px rgba(20, 24, 20, 0.28);\n animation: sheet-up 0.22s ease;\n }\n @keyframes sheet-up { from { transform: translateY(30%); opacity: 0.4; } to { transform: none; opacity: 1; } }\n .filterbar.open .sheet-head {\n display: flex; justify-content: space-between; align-items: center;\n font-family: var(--font-display); font-weight: 700; font-size: 17px;\n letter-spacing: -0.01em; color: var(--navy);\n position: sticky; top: -16px; background: var(--surface); padding: 6px 0 8px;\n border-bottom: 1px solid var(--line); margin-bottom: 2px; z-index: 1;\n }\n .sheet-close {\n border: 1.5px solid var(--ink); background: var(--surface); border-radius: 50%;\n width: 36px; height: 36px; font-size: 15px; cursor: pointer; line-height: 1; color: var(--navy);\n }\n .filterbar.open .sheet-apply { display: block; width: 100%; }\n .sheet-backdrop {\n display: block; position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.45); backdrop-filter: blur(2px);\n }\n\n /* Bouton flottant */\n .fab {\n display: flex; align-items: center; gap: 6px;\n position: fixed; left: 50%; transform: translateX(-50%);\n bottom: calc(74px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: 80;\n background: var(--orange); color: var(--white); border: none;\n border-radius: var(--r-pill); padding: 13px 24px; font-family: var(--font-display);\n font-weight: 600; font-size: 15px; cursor: pointer;\n box-shadow: 0 10px 28px rgba(255, 106, 0, 0.4);\n }\n .fab:active { transform: translateX(-50%) scale(0.97); }\n\n /* Fiche logement : vignettes en bande défilante + panneau non collant */\n .thumbs { display: flex; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }\n .thumbs::-webkit-scrollbar { display: none; }\n .thumbs button { flex: 0 0 96px; }\n .detail { padding-top: 20px; }\n .panel { padding: 20px; }\n .panel .price { font-size: 28px; }\n .results-head { margin-top: 20px; }\n .chips { margin-right: -16px; padding-right: 16px; }\n .notice { padding: 48px 16px; }\n}\n\n/* ================= Barre de navigation inférieure (mobile) ================= */\n.tabbar { display: none; }\n@media (max-width: 760px) {\n .tabbar {\n display: grid; grid-template-columns: repeat(4, 1fr);\n position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border-top: 1px solid var(--line);\n padding: 6px 4px calc(6px + env(safe-area-inset-bottom));\n }\n .tabbar a {\n display: flex; flex-direction: column; align-items: center; gap: 3px;\n padding: 5px 2px; min-height: 48px; justify-content: center;\n font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n border-radius: var(--r-ctl); transition: color 0.12s ease;\n }\n .tabbar a.active { color: var(--orange); }\n .tabbar a svg { display: block; }\n body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }\n .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }\n}\n\n/* Anti-zoom iOS : les champs doivent faire >= 16px\n (filet global : tokens.css force aussi 16 px au doigt) */\n@media (max-width: 900px) {\n .field input, .field select,\n .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }\n}\n\n/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */\n@media (pointer: coarse) {\n .brand { position: relative; }\n .brand::after { content: \"\"; position: absolute; inset: -8px; }\n}\n\n/* ================= Footer ================= */\n.footer { background: var(--navy); color: rgba(255, 255, 255, 0.72); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; }\n.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 32px; color: var(--white); letter-spacing: -0.03em; margin-bottom: 4px; display: flex; align-items: center; }\n.footer .fbrand .logo-ico { margin-right: 13px; }\n.footer .fbrand .ka { color: var(--orange); }\n.footer .fbaseline { color: rgba(255, 255, 255, 0.65); font-size: 14px; margin-bottom: 16px; }\n.footer .fbaseline .hl { color: var(--orange); font-weight: 600; }\n.footer b { color: var(--white); }\n.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }\n.footer .fmono { font-size: 11.5px; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.42); margin-top: 18px; }\n\n/* --- Carte interactive (mission 4) --------------------------------------- */\n.results-tools { display: flex; align-items: center; gap: 14px; }\n.view-toggle {\n display: inline-flex; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); overflow: hidden; background: var(--surface);\n box-shadow: var(--shadow-flat);\n}\n.view-toggle button {\n border: 0; background: transparent; padding: 7px 16px; cursor: pointer;\n font-size: 12.5px; font-weight: 600; color: var(--ink-2);\n}\n/* zone tactile >= 44 px au doigt (coarse pointer = mobile/tablette) */\n@media (pointer: coarse) {\n .view-toggle button { min-height: 44px; padding: 10px 18px; }\n}\n.view-toggle button + button { border-left: 1px solid var(--line); }\n.view-toggle button.on { background: var(--blue); color: var(--white); }\n\n.map-split {\n display: grid; grid-template-columns: minmax(300px, 400px) 1fr; gap: 18px;\n height: calc(100dvh - 170px); min-height: 420px;\n}\n.map-list {\n overflow-y: auto; display: flex; flex-direction: column; gap: 14px;\n padding-right: 4px; scrollbar-width: thin;\n}\n.map-list .card { margin: 0; flex: 0 0 auto; }\n.mapview {\n width: 100%; height: 100%; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.map-loading {\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-3); font-size: 13px;\n}\n\n/* mobile d'abord : carte plein écran, liste masquée (bascule via l'onglet) */\n@media (max-width: 780px) {\n .map-split { grid-template-columns: 1fr; height: calc(100dvh - 210px); }\n .map-list { display: none; }\n .results-tools { width: 100%; justify-content: space-between; }\n}\n\n/* popup MapLibre au style Lou-Ka */\n.maplibregl-popup-content {\n padding: 0; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); box-shadow: var(--shadow-off);\n font-family: var(--font-body);\n}\n.maplibregl-popup-close-button {\n font-size: 18px; padding: 2px 8px; color: var(--white); z-index: 2;\n text-shadow: 0 0 4px rgba(20, 24, 20, 0.8);\n}\n.mv-pop img, .mv-noimg { width: 100%; height: 130px; object-fit: cover; }\n.mv-noimg { display: flex; align-items: center; justify-content: center;\n font-size: 34px; background: var(--grey); }\n.mv-pop-body { padding: 10px 12px 12px; }\n.mv-pop-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--navy); }\n.mv-pop-price small { font-weight: 400; color: var(--ink-3); font-size: 12px; }\n.mv-pop-title { font-size: 13px; color: var(--ink-2); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mv-pop-meta { font-size: 11.5px; color: var(--ink-2); margin-top: 6px; }\n.mv-pop-src { font-size: 10px; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }\n.mv-pop-cta {\n display: block; margin-top: 10px; padding: 9px 10px; text-align: center;\n background: var(--orange); color: var(--white); border-radius: var(--r-pill);\n font-weight: 600; font-size: 13px;\n}\n.mv-pop-cta:hover { background: var(--orange-deep); }\n\n/* --- Commodités de proximité (fiche) -------------------------------------- */\n.poi-list {\n list-style: none; margin: 0; padding: 0;\n display: flex; flex-direction: column; gap: 2px;\n}\n.poi-list li {\n display: flex; align-items: center; gap: 9px;\n padding: 5px 2px; border-bottom: 1px solid var(--line);\n font-size: 13px;\n}\n.poi-list li:last-child { border-bottom: 0; }\n.poi-ico { width: 20px; text-align: center; flex: 0 0 auto; }\n.poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.poi-dist { font-size: 11.5px; color: var(--navy); font-weight: 600; flex: 0 0 auto; }\n\n/* --- Menu déroulant mobile ------------------------------------------------- */\n.menu-btn {\n display: none; position: relative; z-index: 60;\n width: 44px; height: 44px; margin-left: auto;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); cursor: pointer; padding: 0;\n flex-direction: column; align-items: center; justify-content: center; gap: 5px;\n box-shadow: var(--shadow-flat);\n transition: box-shadow 0.15s ease, transform 0.15s ease;\n}\n.menu-btn:active { transform: scale(0.96); }\n.menu-btn span {\n display: block; width: 18px; height: 2px; background: var(--navy);\n border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease;\n}\n.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }\n.menu-btn.open span:nth-child(2) { opacity: 0; }\n.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }\n\n.mobile-menu {\n display: none; position: absolute; left: 0; right: 0; top: 100%;\n background: var(--white); border-bottom: 1px solid var(--line);\n box-shadow: 0 18px 30px rgba(20, 24, 20, 0.14);\n padding: 8px 16px calc(16px + env(safe-area-inset-bottom));\n max-height: 0; overflow: hidden; opacity: 0;\n transition: max-height 0.3s ease, opacity 0.22s ease;\n}\n.mobile-menu { z-index: var(--z-modal, 900); }\n.mobile-menu.open { max-height: 480px; opacity: 1; }\n/* Le drawer vit DANS le header : quand il est ouvert, le header monte au niveau\n modal pour peindre au-dessus du .mm-backdrop (frère du header, overlay 800). */\n.header:has(.mobile-menu.open) { z-index: var(--z-modal, 900); }\n.mm-link {\n display: flex; align-items: center; gap: 12px;\n padding: 15px 10px; min-height: 52px;\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n letter-spacing: -0.02em; border-bottom: 1px solid var(--line);\n color: var(--navy);\n opacity: 0; transform: translateY(-8px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); }\n.mm-link.active { color: var(--orange); }\n.mm-link.active .mm-arrow { opacity: 1; color: var(--orange); }\n.mm-ico { width: 26px; text-align: center; font-size: 17px; }\n.mm-arrow { margin-left: auto; opacity: 0.25; color: var(--orange); transition: opacity 0.15s ease; }\n.mm-link:active { background: var(--orange-soft); border-radius: var(--r-ctl); }\n.mm-foot {\n padding: 12px 10px 4px;\n font-size: 11px; color: var(--ink-3); letter-spacing: 0.02em;\n}\n.mm-backdrop {\n position: fixed; inset: 0; z-index: var(--z-overlay, 800);\n background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px);\n}\n@media (max-width: 760px) {\n .menu-btn { display: flex; }\n .nav { display: none; }\n .mobile-menu { display: block; }\n}\n\n/* --- Bandeau de consentement (témoins) ------------------------------------- */\n.cookie-banner {\n position: fixed; left: 12px; right: 12px;\n bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80;\n animation: cookie-in 0.35s ease;\n}\n@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } }\n.cookie-inner {\n max-width: 720px; margin: 0 auto; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 16px 18px;\n}\n.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }\n.cookie-text b { color: var(--navy); }\n.cookie-text a { text-decoration: underline; text-underline-offset: 2px; }\n.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n.cookie-opt {\n display: flex; align-items: flex-start; gap: 10px; font-size: 13px;\n color: var(--ink-2); cursor: pointer;\n}\n.cookie-opt input { margin-top: 2px; accent-color: var(--orange); width: 16px; height: 16px; }\n.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; }\n@media (max-width: 640px) {\n .cookie-actions { justify-content: stretch; }\n .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; }\n}\n\n/* --- Page confidentialité + lien pied de page ------------------------------- */\n.legal { padding: 48px 0 70px; max-width: 760px; }\n.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; color: var(--navy); }\n.legal section { margin-top: 26px; }\n.legal h3 { font-size: 18px; margin-bottom: 8px; }\n.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }\n.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }\n.flink {\n border: 0; background: none; padding: 0; cursor: pointer;\n color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px;\n}\n\n/* --- Section « Le quartier » (fiche) --------------------------------------- */\n.quartier { margin-top: 34px; }\n.quartier h2 { font-size: 24px; letter-spacing: -0.02em; color: var(--navy); }\n.q-sub { color: var(--ink-3); font-size: 13px; margin: 4px 0 16px; }\n.q-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;\n}\n@media (max-width: 640px) { .q-grid { grid-template-columns: repeat(2, 1fr); } }\n.q-cell {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 12px 14px; box-shadow: var(--shadow-flat);\n}\n.q-val { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.q-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3);\n text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px; }\n.q-prox { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }\n.q-bar { display: flex; align-items: center; gap: 10px; font-size: 13px; }\n.q-bar-label { flex: 0 0 150px; color: var(--ink-2); }\n@media (max-width: 640px) { .q-bar-label { flex-basis: 120px; font-size: 12px; } }\n.q-bar-track {\n flex: 1; height: 10px; background: var(--grey);\n border-radius: var(--r-pill); overflow: hidden;\n}\n.q-bar-fill { display: block; height: 100%; background: var(--blue); border-radius: var(--r-pill); }\n.q-bar-num { flex: 0 0 30px; text-align: right;\n font-size: 11.5px; font-weight: 700; color: var(--navy); }\n.q-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n.q-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 7px 14px; font-size: 12.5px; background: var(--surface);\n}\n.q-badge.cool { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.q-badge.hot { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.quartier .fine { margin-top: 10px; }\n\n/* --- Fiche v2 (mobile-first) ------------------------------------------------ */\n/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka\n « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`\n / `display:contents` pour réordonner — les blocs sans `order` retombaient à\n order:0 et passaient devant la galerie sur mobile. Mobile : colonnes\n empilées ; desktop : grid 2 colonnes (logement | synthèse). */\n.fiche { display: flex; flex-direction: column; gap: 22px; }\n.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\n@media (min-width: 900px) {\n .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }\n .f-col { gap: 26px; }\n}\n.f-bloc { min-width: 0; }\n.f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; color: var(--navy); }\n.f-bloc:empty { display: none; }\n\n/* galerie à balayage natif */\n.carousel { position: relative; border-radius: var(--r-card); overflow: hidden;\n border: 1.5px solid var(--ink); background: var(--grey); box-shadow: var(--shadow-off-soft); }\n.carousel-track {\n display: flex; overflow-x: auto; scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch; scrollbar-width: none; aspect-ratio: 16/11;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track img {\n flex: 0 0 100%; width: 100%; object-fit: cover; scroll-snap-align: center;\n cursor: zoom-in;\n}\n.carousel-empty { display: flex; align-items: center; justify-content: center;\n aspect-ratio: 16/11; font-size: 48px; }\n.carousel-count {\n position: absolute; right: 12px; bottom: 12px; z-index: 2;\n background: rgba(20, 24, 20, 0.78); color: var(--white);\n font-size: 12px; font-weight: 600;\n padding: 4px 11px; border-radius: var(--r-pill);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);\n}\n.carousel-nav {\n position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;\n width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--ink);\n background: rgba(255, 255, 255, 0.92); color: var(--navy); font-size: 22px; cursor: pointer;\n display: flex; align-items: center; justify-content: center; line-height: 1;\n box-shadow: var(--shadow-flat);\n}\n.carousel-nav.prev { left: 10px; } .carousel-nav.next { right: 10px; }\n@media (max-width: 640px) { .carousel-nav { display: none; } } /* balayage natif */\n\n/* bandeau prix + badge marché */\n.f-hero .price { font-size: clamp(34px, 8vw, 44px); }\n.deal-badge {\n display: inline-flex; align-items: center; gap: 6px; margin: 8px 0 4px;\n border-radius: var(--r-pill); padding: 7px 14px; font-size: 13px; font-weight: 600;\n border: 1.5px solid var(--ink);\n}\n.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n.deal-ok { background: var(--surface); color: var(--ink-2); }\n.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.35); color: var(--orange-deep); }\n.f-hero h1 { font-size: clamp(20px, 4.5vw, 26px); margin-top: 8px; }\n\n/* chips clés à défilement horizontal */\n.chips-scroll {\n display: flex; gap: 8px; overflow-x: auto; padding: 12px 0 6px;\n scrollbar-width: none; -webkit-overflow-scrolling: touch;\n}\n.chips-scroll::-webkit-scrollbar { display: none; }\n.chip-key {\n flex: 0 0 auto; background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 8px 15px;\n font-size: 12px; font-weight: 600; white-space: nowrap; min-height: 34px;\n display: inline-flex; align-items: center;\n}\n\n/* ancres de navigation rapide */\n.ancres {\n display: flex; gap: 4px; overflow-x: auto; padding: 6px 0 2px;\n scrollbar-width: none; border-bottom: 1px solid var(--line); margin-bottom: 8px;\n}\n.ancres::-webkit-scrollbar { display: none; }\n.ancres a {\n flex: 0 0 auto; padding: 9px 14px; font-size: 13.5px; font-weight: 600;\n color: var(--ink-2); border-radius: var(--r-pill); min-height: 44px;\n display: inline-flex; align-items: center;\n}\n.ancres a:hover { background: var(--orange-soft); color: var(--orange); }\nhtml { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */\n\n/* description restructurée */\n.enbref {\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-card); padding: 12px 16px; color: var(--navy);\n font-size: 15px; margin: 0 0 14px;\n}\n.desc-section { margin-bottom: 12px; }\n.desc-section h4 { font-size: 15px; margin-bottom: 3px; }\n.desc-section p { color: var(--ink-2); font-size: 14.5px; margin: 0; white-space: pre-line; }\n.texte-original { margin-top: 12px; }\n.texte-original summary {\n cursor: pointer; font-size: 12.5px;\n color: var(--ink-3); min-height: 44px; display: flex; align-items: center;\n}\n.texte-original p { color: var(--ink-3); font-size: 13px; white-space: pre-line; }\n\n/* inclusions : confirmées vs mentionnées */\n.amenity.confirmed { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.25); color: var(--blue); font-weight: 600; }\n.amenity.unconfirmed { background: transparent; border-style: dashed; border-color: var(--line-strong); color: var(--ink-3); }\n\n/* détails pratiques : historique de prix */\n.prix-histo {\n margin-top: 12px; padding: 10px 14px; border-radius: var(--r-card);\n border: 1.5px solid var(--ink); font-size: 13.5px; background: var(--surface);\n}\n.prix-histo.down { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); }\n\n/* quartier compacté sur mobile */\n@media (max-width: 640px) {\n .q-cell { padding: 9px 11px; }\n .q-val { font-size: 16px; }\n .q-bar { gap: 8px; font-size: 12px; }\n .q-bar-track { height: 8px; }\n}\n.q-badge-sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }\n\n/* POI groupés repliables */\n.poi-groupe { border: 1.5px solid var(--ink); border-radius: var(--r-card);\n margin-bottom: 8px; background: var(--surface); overflow: hidden; }\n.poi-groupe summary {\n display: flex; justify-content: space-between; align-items: center; gap: 10px;\n padding: 12px 14px; cursor: pointer; font-weight: 600; font-size: 14px;\n min-height: 48px; list-style: none;\n}\n.poi-groupe summary::-webkit-details-marker { display: none; }\n.poi-groupe summary::after { content: \"▾\"; color: var(--ink-3); transition: transform 0.15s ease; }\n.poi-groupe[open] summary::after { transform: rotate(180deg); }\n.poi-resume { font-size: 11.5px; color: var(--ink-3); font-weight: 400; }\n.poi-groupe .poi-list { padding: 0 14px 10px; }\n\n/* CTA sticky mobile */\n.cta-sticky {\n position: fixed; left: 0; right: 0; z-index: var(--z-dropdown, 700);\n bottom: var(--consent-h, 0px);\n display: flex; align-items: center; gap: 12px;\n background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(12px);\n border-top: 1px solid var(--line);\n padding: 10px 16px calc(10px + env(safe-area-inset-bottom));\n}\n.cta-sticky .cta { flex: 1; margin: 0; text-align: center; }\n.cta-sticky-prix { font-family: var(--font-display); font-weight: 700; font-size: 19px; white-space: nowrap; color: var(--navy); }\n.cta-sticky-prix small { font-weight: 500; font-size: 10px; color: var(--ink-3); }\n@media (min-width: 900px) { .cta-sticky { display: none; } }\n@media (max-width: 899px) {\n .cta-desktop { display: none; }\n .detail { padding-bottom: 84px; } /* place pour le CTA sticky */\n}\n.f-foot { margin-top: 26px; }\n\n/* --- Passerelle vers l'annonce originale ------------------------------------ */\n.passerelle {\n min-height: calc(100dvh - 120px); display: flex; align-items: center;\n justify-content: center; padding: 24px 16px;\n}\n.pass-card {\n width: 100%; max-width: 520px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n box-shadow: var(--shadow-off); padding: 30px 28px; text-align: center;\n}\n.pass-brands {\n display: flex; align-items: center; justify-content: center; gap: 14px;\n margin-bottom: 22px; flex-wrap: wrap;\n}\n.pass-brand {\n font-family: var(--font-display); font-weight: 700; font-size: 24px;\n letter-spacing: -0.03em; display: inline-flex; align-items: center; color: var(--navy);\n}\n.pass-brand .ka { color: var(--orange); }\n.pass-trajet { display: inline-flex; align-items: center; gap: 5px; }\n.pass-dot {\n width: 6px; height: 6px; border-radius: 50%; background: var(--orange);\n opacity: 0.25; animation: pass-dot 1.2s ease infinite;\n}\n.pass-dot:nth-child(2) { animation-delay: 0.2s; }\n.pass-dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes pass-dot { 40% { opacity: 1; transform: translateX(2px); } }\n.pass-fleche { color: var(--orange); font-size: 18px; }\n.pass-gestionnaire {\n font-family: var(--font-display); font-weight: 700; font-size: 19px;\n display: inline-flex; align-items: center; gap: 8px;\n background: var(--blue-soft); border: 1px solid rgba(28, 92, 65, 0.25);\n border-radius: var(--r-pill); padding: 6px 16px; color: var(--blue);\n}\n.pass-card h1 { font-size: 20px; letter-spacing: -0.02em; }\n.pass-texte { color: var(--ink-2); font-size: 14.5px; margin: 10px 0 20px; }\n.pass-progress {\n height: 10px; border-radius: var(--r-pill);\n overflow: hidden; background: var(--grey); margin-bottom: 20px;\n}\n.pass-progress-fill {\n display: block; height: 100%; width: 0; background: var(--orange);\n border-radius: var(--r-pill);\n}\n.pass-progress-fill.go { animation: pass-fill 2.2s linear forwards; }\n@keyframes pass-fill { to { width: 100%; } }\n@media (prefers-reduced-motion: reduce) {\n .pass-progress-fill.go { animation: none; width: 100%; }\n .pass-dot { animation: none; opacity: 0.7; }\n}\n.pass-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; }\n\n/* --- boutons PDF -------------------------------------------------------- */\n.btn-pdf { display: block; text-align: center; margin-top: 10px; width: 100%; }\n\n/* --- Onglets de la page Stats -------------------------------------------- */\n.onglets {\n display: flex; gap: 8px; overflow-x: auto; margin: 26px 0 22px;\n padding-bottom: 4px; scrollbar-width: none;\n border-bottom: 1px solid var(--line);\n}\n.onglets::-webkit-scrollbar { display: none; }\n.onglet {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;\n border: 1.5px solid var(--ink);\n border-radius: var(--r-pill);\n background: var(--surface); color: var(--ink-2); cursor: pointer;\n padding: 10px 18px; font-weight: 600; font-size: 13.5px; min-height: 44px;\n margin-bottom: 8px;\n transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;\n}\n.onglet:hover { background: var(--blue-soft); color: var(--blue); border-color: var(--blue); }\n.onglet.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* baisses de prix */\n.baisses { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }\n.baisses li {\n display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n padding: 9px 4px; border-bottom: 1px solid var(--line); font-size: 13.5px;\n}\n.baisses li a { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }\n.baisse-ville { color: var(--ink-3); font-size: 11.5px; }\n.baisse-prix { margin-left: auto; }\n.baisse-prix s { color: var(--ink-3); }\n.baisse-pct {\n font-style: normal; font-weight: 700;\n background: var(--blue-soft);\n color: var(--blue); border-radius: var(--r-pill); padding: 2px 9px;\n font-size: 11px; margin-left: 8px;\n}\n.alertes { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n\n/* ================= Pagination (12 annonces/page) ================= */\n.pager {\n display: flex; align-items: center; justify-content: space-between;\n gap: 16px; flex-wrap: wrap; margin: 26px 0 10px; padding-top: 18px;\n border-top: 1px solid var(--line);\n}\n.pager-info {\n font-size: 12.5px; color: var(--ink-2); letter-spacing: 0.02em;\n}\n.pager-info em { font-style: normal; color: var(--ink-3); }\n.pager-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.pager-btn {\n min-width: 38px; height: 38px; padding: 0 10px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); cursor: pointer;\n font-size: 13px; font-weight: 600;\n color: var(--navy); transition: all 0.12s ease;\n}\n.pager-btn:hover:not(:disabled):not(.on) {\n background: var(--blue-soft); border-color: var(--blue); color: var(--blue);\n}\n.pager-btn.on { background: var(--navy); border-color: var(--navy); color: var(--white); cursor: default; }\n.pager-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n.pager-ellipsis { color: var(--ink-3); padding: 0 2px; }\n.map-list .pager { margin: 10px 0 4px; }\n@media (max-width: 640px) {\n .pager { justify-content: center; }\n .pager-info { width: 100%; text-align: center; order: 2; }\n}\n\n/* ================= Icônes SVG inline — alignement ================= */\n.chip-ico { display: inline-flex; align-items: center; gap: 6px; }\n.chip-ico svg { flex: none; }\n.view-toggle button, .seg button { display: inline-flex; align-items: center; gap: 6px; }\n.fab { align-items: center; gap: 8px; }\n.fab svg { flex: none; }\n.mm-ico { display: inline-flex; align-items: center; }\n.notice .big { display: flex; justify-content: center; color: var(--ink-3); }\n.notice .big svg { display: block; }\n.card-img .noimg { color: var(--ink-3); }\n.badge.right { display: inline-flex; align-items: center; gap: 4px; }\n.btn-pdf { display: inline-flex; align-items: center; gap: 7px; }\n.f-search svg { flex: none; color: var(--ink-3); }\n.mv-noimg { display: flex; align-items: center; justify-content: center; color: var(--ink-3); }\n\n/* footer — liens (Groupe KA, courriels, mentions légales) */\n.footer .frow a { color: var(--white); text-decoration: underline; text-decoration-color: rgba(255, 106, 0, 0.5); text-underline-offset: 3px; }\n.footer .frow a:hover { color: var(--orange); }\n.footer .fmono a:hover { color: var(--orange); }\n\n/* ================= Compte (connexion Google) ================= */\n.login-btn {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 8px 16px; margin-left: 10px; min-height: 38px;\n background: var(--navy); border: none;\n border-radius: var(--r-pill); font-weight: 600; font-size: 13.5px; color: var(--white);\n transition: all 0.12s ease; white-space: nowrap;\n box-shadow: 0 4px 14px rgba(20, 24, 20, 0.22);\n}\n.login-btn:hover { background: var(--blue); }\n.account { position: relative; margin-left: 10px; }\n.account-btn {\n width: 38px; height: 38px; padding: 0; border: 2px solid var(--orange);\n border-radius: var(--r-pill); overflow: hidden; cursor: pointer;\n background: var(--orange-soft); display: flex; align-items: center; justify-content: center;\n}\n.account-btn img { width: 100%; height: 100%; object-fit: cover; }\n.account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }\n/* Click-catcher plein écran rendu via createPortal à la racine de \n (un fixed descendant du header blurré se re-parentait → 66 px de haut).\n z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le\n header et doit peindre au-dessus du voile. */\n.account-backdrop { position: fixed; inset: 0; z-index: 40; }\n.account-menu {\n position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;\n min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off); overflow: hidden;\n}\n.account-id { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-bottom: 1px solid var(--line); }\n.account-id b { font-size: 14px; }\n.account-id span { font-size: 11.5px; color: var(--ink-3); word-break: break-all; }\n.account-menu button {\n width: 100%; text-align: left; padding: 11px 14px; border: 0; cursor: pointer;\n background: transparent; font-weight: 600; font-size: 13.5px; color: var(--danger);\n}\n.account-menu button:hover { background: var(--orange-soft); color: var(--navy); }\n@media (max-width: 640px) { .login-btn { padding: 8px 12px; font-size: 12.5px; } }\n\n/* ================= Profil — carte de membre Groupe KA ================= */\n.profil { padding-top: 40px; padding-bottom: 60px; }\n.profil h1 { font-size: clamp(30px, 5vw, 44px); margin: 10px 0 26px; color: var(--navy); }\n.profil .lede { max-width: 520px; color: var(--ink-2); margin-bottom: 22px; }\n\n.pc {\n position: relative; max-width: 520px; overflow: hidden;\n background: linear-gradient(135deg, #101a40 0%, var(--navy) 55%, #080e24 100%);\n color: var(--white);\n border-radius: 20px;\n padding: 24px 26px 0; box-shadow: var(--shadow-off);\n}\n.pc-watermark {\n position: absolute; right: -18px; top: -34px; pointer-events: none;\n font-family: var(--font-display); font-weight: 700; font-size: 170px;\n letter-spacing: -0.06em; color: rgba(255, 106, 0, 0.07);\n transform: rotate(-8deg); line-height: 1;\n}\n.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }\n.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }\n.pc-ka { color: var(--orange); margin-left: 1px; display: inline-block; }\n.pc-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.5); }\n.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }\n.pc-id-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2em; color: rgba(255, 106, 0, 0.75); }\n.pc-id {\n font-weight: 700; color: var(--orange);\n font-size: clamp(22px, 6vw, 34px); letter-spacing: 0.14em;\n text-shadow: 0 0 24px rgba(255, 106, 0, 0.35);\n}\n.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }\n.pc-holder { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pc-holder-name { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pc-holder-since { font-size: 10.5px; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.06em; }\n.pc-role-badge {\n display: inline-block; width: fit-content; margin: 3px 0 2px;\n background: var(--orange); color: var(--white);\n font-size: 9px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.12em;\n padding: 2px 8px 3px; border-radius: var(--r-pill);\n}\n.pc-avatar { width: 52px; height: 52px; border-radius: var(--r-pill); border: 2px solid var(--orange); flex: none; }\n.pc-strip { display: flex; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(255, 106, 0, 0.1); border-top: 1px solid rgba(255, 106, 0, 0.25); overflow: hidden; }\n.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--orange); opacity: 0.7; }\n.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.35; }\n.pc-strip i:nth-child(3n+1) { height: 9px; }\n.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.9; }\n.pc-copy { margin-top: 16px; }\n.pc-copy.ok { background: var(--blue); color: var(--white); }\n\n.profil-grid {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 12px; margin-top: 30px; max-width: 760px;\n}\n.pg-item {\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); padding: 14px 16px;\n display: flex; flex-direction: column; gap: 4px; box-shadow: var(--shadow-flat);\n}\n.pg-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }\n.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }\n.pg-value.mono { color: var(--blue); }\n\n.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }\n.profil-note a { text-decoration: underline; text-underline-offset: 3px; }\n.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }\n.profil-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n\n.account-kaid { font-size: 10.5px; color: var(--blue); letter-spacing: 0.08em; }\n.account-link {\n display: block; width: 100%; text-align: left; padding: 11px 14px;\n font-weight: 600; font-size: 13.5px; color: var(--navy);\n border-bottom: 1px solid var(--line);\n}\n.account-link:hover { background: var(--orange-soft); }\n\n/* profil public — section + lien partageable */\n.profil-public { margin-top: 30px; max-width: 640px; }\n.profil-public h3 { font-size: 18px; margin-bottom: 6px; }\n.profil-public p { color: var(--ink-2); font-size: 13.5px; margin: 0 0 12px; }\n.public-link {\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n margin-top: 14px; padding: 12px 14px; background: var(--blue-soft);\n border: 1px solid rgba(28, 92, 65, 0.25); border-radius: var(--r-card);\n}\n.public-link a { font-size: 13px; color: var(--blue); text-decoration: underline; text-underline-offset: 3px; word-break: break-all; }\n\n/* ================= Bienvenue — choix du rôle ================= */\n.bienvenue { padding-top: 44px; padding-bottom: 70px; }\n.bienvenue h1 { font-size: clamp(30px, 5vw, 46px); margin: 10px 0 14px; color: var(--navy); }\n.bienvenue .lede { max-width: 480px; color: var(--ink-2); margin-bottom: 30px; }\n.role-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; max-width: 780px; }\n.role-card {\n display: flex; flex-direction: column; align-items: flex-start; gap: 10px;\n text-align: left; padding: 24px 24px 20px; cursor: pointer;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-flat);\n font-family: var(--font-body); transition: all 0.15s ease;\n}\n.role-card:hover:not(:disabled) { transform: translateY(-4px); box-shadow: var(--shadow-off); border-color: rgba(255, 106, 0, 0.4); }\n.role-card:disabled { opacity: 0.6; cursor: wait; }\n.role-ico { display: flex; width: 54px; height: 54px; align-items: center; justify-content: center; background: var(--orange-soft); color: var(--orange); border-radius: 16px; }\n.role-title { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; color: var(--navy); }\n.role-desc { color: var(--ink-2); font-size: 13.5px; }\n.role-perks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }\n.role-perks li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }\n.role-perks li::before {\n content: \"✓\"; flex: none; width: 16px; height: 16px; border-radius: 50%;\n background: var(--orange); color: var(--white); font-size: 10px; font-weight: 700;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.role-cta { margin-top: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--orange); }\n\n/* ================= Favoris — cœur sur les cartes ================= */\n.fav-btn {\n position: absolute; right: 10px; bottom: 10px; z-index: 2;\n width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;\n background: rgba(255, 255, 255, 0.94); border: none;\n border-radius: var(--r-pill); cursor: pointer; color: var(--navy);\n box-shadow: var(--shadow-flat);\n transition: transform 0.12s ease, background 0.12s ease, color 0.12s ease;\n}\n.fav-btn:hover { transform: scale(1.12); }\n.fav-btn.on { background: var(--white); color: var(--orange); }\n.fav-btn.on svg { filter: drop-shadow(0 0 3px rgba(255, 106, 0, 0.35)); }\n\n/* ================= Espace gestionnaire ================= */\n.claim-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; max-width: 640px; }\n.claim-row select {\n flex: 1; min-width: 260px; padding: 11px 14px; font-size: 14px;\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); font-family: var(--font-body);\n}\n.claim-error { color: var(--danger); font-size: 13.5px; font-weight: 600; margin-top: 10px; }\n.org-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 22px; }\n.org-stat { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); }\n.org-form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }\n.org-form h3 { font-size: 18px; }\n.org-form label { display: flex; flex-direction: column; gap: 5px; flex: 1; }\n.org-form label span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }\n.org-form input, .org-form textarea {\n padding: 10px 14px; font-size: 14px; font-family: var(--font-body);\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);\n}\n.org-form textarea { resize: vertical; }\n.org-form input:focus, .org-form textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(28, 92, 65, 0.15); }\n.org-form-row { display: flex; gap: 12px; flex-wrap: wrap; }\n.org-form .btn { align-self: flex-start; }\n\n/* ================= Page publique /g/{source} ================= */\n.orgpub-head { display: flex; gap: 20px; align-items: flex-start; margin-top: 8px; }\n.orgpub-logo { width: 84px; height: 84px; object-fit: contain; background: var(--surface); border: 1.5px solid var(--ink); border-radius: 18px; padding: 8px; flex: none; box-shadow: var(--shadow-flat); }\n.orgpub-id h1 { font-size: clamp(26px, 4.4vw, 40px); margin: 6px 0 8px; color: var(--navy); }\n.orgpub-tagline { font-size: 16px; color: var(--ink-2); margin: 0 0 12px; max-width: 640px; }\n.orgpub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.orgpub-meta a.stat-chip:hover { background: var(--blue-soft); }\n.orgpub-verified { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.3); color: var(--blue); font-weight: 600; }\n.orgpub-desc { max-width: 720px; color: var(--ink-2); margin: 18px 0 6px; white-space: pre-line; }\n.orgpub .results-head h2 { display: inline-flex; align-items: center; gap: 8px; }\n@media (max-width: 560px) { .orgpub-head { flex-direction: column; } }\n\n/* ================= Éditeur de profil (photo, bio, réseaux) ================= */\n.profil-edit { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--line-strong); }\n/* profil géré au hub Groupe KA — lecture seule */\n.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }\n.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }\n.hub-profile a.stat-chip:hover { background: var(--blue-soft); }\n.hub-profile .btn-primary { display: inline-flex; align-items: center; gap: 7px; }\n.pe-avatar-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.pe-avatar {\n width: 84px; height: 84px; border-radius: var(--r-pill); object-fit: cover;\n border: 2.5px solid var(--orange); box-shadow: var(--shadow-flat);\n display: flex; align-items: center; justify-content: center;\n}\n.pe-avatar-empty { background: var(--orange-soft); color: var(--orange); font-family: var(--font-display); font-weight: 700; font-size: 34px; }\n.pe-avatar-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }\n.pe-avatar-actions .btn { display: inline-flex; align-items: center; gap: 7px; }\n.pe-hint { font-size: 11px; color: var(--ink-3); }\n.pe-socials-title { font-size: 15px; margin-top: 6px; }\n.pe-socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }\n.pe-social { display: flex !important; flex-direction: row !important; align-items: center; gap: 0 !important; }\n.pe-social-ico {\n display: flex; align-items: center; justify-content: center;\n width: 40px; align-self: stretch; flex: none;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-ctl) 0 0 var(--r-ctl);\n}\n.pe-social input { flex: 1; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; border-left: 0; }\n\n/* profil public — à propos + réseaux */\n.pub-about { max-width: 640px; margin-top: 22px; }\n.pub-bio { font-size: 15.5px; color: var(--ink); margin: 0 0 12px; white-space: pre-line; }\n.pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }\n.pub-socials { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }\n.pub-social {\n display: flex; align-items: center; justify-content: center;\n width: 44px; height: 44px; background: var(--surface);\n border: 1.5px solid var(--ink); border-radius: 14px; color: var(--navy);\n transition: all 0.12s ease;\n}\n.pub-social:hover { background: var(--navy); border-color: var(--navy); color: var(--white); transform: translateY(-2px); box-shadow: var(--shadow-off-soft); }\n\n/* -----------------------------------------------------------------------------\n Lou-Ka Maps (framework Ka Maps par Groupe Ka)\n Jetons du design system appliqués au chrome de la carte + sync liste↔carte.\n----------------------------------------------------------------------------- */\n.mapview .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n}\n.mapview { position: relative; overflow: hidden; }\n.mapview .ka-map { position: absolute; inset: 0; }\n\n/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en orange */\n.lmap3d {\n position: relative; height: 340px; border: 1.5px solid var(--ink);\n border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);\n overflow: hidden; background: var(--grey);\n}\n.lmap3d .ka-map {\n --ka-accent: var(--accent);\n --ka-on-accent: var(--on-accent);\n --ka-surface: var(--surface);\n --ka-ink: var(--navy);\n --ka-line: var(--line-strong);\n --ka-radius: var(--r-ctl);\n --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14);\n --ka-font: var(--font-body);\n position: absolute; inset: 0;\n}\n.lmap3d-skel {\n background: linear-gradient(90deg, #ececef 25%, #f7f7f9 50%, #ececef 75%);\n background-size: 800px 100%; animation: shimmer 1.4s infinite linear;\n}\n.lmap3d-legende {\n position: absolute; left: 10px; bottom: 10px; z-index: 5;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 4px 10px; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n font-size: 10.5px; font-weight: 500; color: var(--ink-2);\n pointer-events: none;\n}\n.lmap3d-legende i {\n width: 10px; height: 10px; border-radius: 3px;\n background: var(--orange); border: 1px solid rgba(20, 24, 20, 0.25);\n}\n@media (max-width: 780px) { .lmap3d { height: 280px; } }\n.ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }\n.ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }\n.ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }\n\n/* carte de liste synchronisée avec la carte (survol / sélection) */\n.map-card { border-radius: var(--r-card); }\n.map-card-sel { outline: 2px solid var(--blue); outline-offset: 2px; }\n\n/* =============================================================================\n HARMONISATION « ÉDITORIAL SHARP » (Groupe KA) — couche finale posée après\n les styles historiques : géométrie 10/6 px, bordures encre 1,5 px, ombres\n décalées dures, accent orange Lou-Ka. La carte Ka Maps (Mapbox) conserve\n son propre thème marine/orange à l'intérieur du canevas.\n============================================================================= */\n\n/* — contrôles : plus de pilules (les chips/badges gardent --r-pill) — */\n.nav a, .login-btn, .cta, .mv-pop-cta, .f-search, .f-more, .pager-btn,\n.onglet, .view-toggle, .fab, .f-ctl, .pass-gestionnaire, .hbar-track,\n.q-bar-track, .pass-progress { border-radius: var(--r-ctl); }\n.seg button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); }\n.seg button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }\n\n/* — header : papier translucide + trait d'encre — */\n.header {\n background: rgba(245, 243, 238, 0.92);\n border-bottom: 1.5px solid var(--ink);\n}\n.nav a { font-family: var(--font-display); }\n.nav a:hover { color: var(--accent-deep); background: var(--accent-soft); }\n.nav a.active { background: var(--ink); color: var(--accent); }\n.brand .ka {\n background: var(--accent); color: var(--on-accent);\n border-radius: 6px; padding: 0 6px 2px; margin-left: 2px; transform: rotate(-2deg);\n}\n.header .gk-badge { margin-left: 4px; flex: none; }\n@media (max-width: 1080px) { .header .gk-badge { display: none; } }\n.mm-badge {\n display: flex; align-items: center; min-height: var(--touch);\n padding: 8px 10px 2px;\n}\n\n/* — ticker : encre + mono — */\n.ticker { font-family: var(--font-mono); border-bottom: 1.5px solid var(--ink); }\n\n/* — états sélectionnés : encre + accent (fini le bleu) — */\n.seg button.on, .onglet.on, .view-toggle button.on, .f-more.on {\n background: var(--ink); border-color: var(--ink); color: var(--accent);\n}\n.pager-btn.on { background: var(--ink); border-color: var(--ink); color: var(--accent); }\n\n/* — CTA / FAB : aplat accent, bordure encre, ombre décalée — */\n.cta, .mv-pop-cta, .fab {\n background: var(--accent); color: var(--on-accent);\n border: 1.5px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);\n font-family: var(--font-display); font-weight: 700;\n}\n.cta:hover, .mv-pop-cta:hover, .fab:hover { background: var(--accent-deep); }\n.cta:active, .fab:active { box-shadow: none; }\n.fab { box-shadow: 4px 4px 0 var(--ink), 0 10px 28px rgba(20, 24, 20, 0.25); }\n.login-btn {\n background: var(--ink); color: var(--accent);\n border: 1.5px solid var(--ink); box-shadow: none;\n font-family: var(--font-display); font-weight: 700;\n}\n.login-btn:hover { background: var(--accent-deep); color: var(--on-accent); }\n\n/* — cartes : levée nette diagonale (signature du groupe) — */\n.card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }\n.role-card:hover:not(:disabled) { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); border-color: var(--ink); }\n.badge { border: 1.5px solid var(--ink); border-radius: var(--r-ctl); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.badge.type { background: var(--accent); color: var(--on-accent); }\n.source-tag { font-family: var(--font-mono); }\n\n/* — barre d'onglets mobile : papier + encre, actif = accent orange — */\n@media (max-width: 760px) {\n .tabbar { background: rgba(245, 243, 238, 0.95); border-top: 1.5px solid var(--ink); }\n .tabbar a { font-family: var(--font-display); }\n .tabbar a.active { color: var(--accent); }\n}\n.mobile-menu { background: var(--paper); border-bottom: 1.5px solid var(--ink); }\n\n/* — carte de membre KA : encre plutôt que marine — */\n.pc { background: linear-gradient(135deg, #1d231d 0%, var(--ink) 55%, #0b0e0b 100%); }\n\n/* — micro-étiquettes en mono (système) — */\n.f-ctl > span, .f-group > label, .field label, .tile-k, .pg-label,\n.pc-label, .pc-id-label, .crumbs, .viz-sub { font-family: var(--font-mono); }\n\n/* — page Contact (design system) — */\n.contact-page { padding: 44px 0 90px; }\n.contact-page h1 { margin: 10px 0 8px; }\n.contact-page .lede { color: var(--ink-2); max-width: 640px; margin: 0 0 26px; }\n.contact-grid { display: grid; gap: 16px; grid-template-columns: 1fr; margin: 26px 0; }\n@media (min-width: 768px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }\n.contact-card { padding: 18px 20px; }\n.contact-card .klabel { display: block; margin-bottom: 6px; }\n.contact-card a.mail {\n font-family: var(--font-mono); font-weight: 700; font-size: 13.5px;\n color: var(--accent-deep); word-break: break-all;\n text-decoration: underline; text-underline-offset: 3px;\n display: inline-flex; align-items: center; min-height: 44px;\n}\n.contact-card p { margin: 6px 0 0; color: var(--ink-2); font-size: 13.5px; }\n.contact-note {\n border-left: 3px solid var(--accent); background: var(--surface);\n border-top: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);\n border-bottom: 1.5px solid var(--ink); border-radius: 0 var(--r-card) var(--r-card) 0;\n padding: 14px 18px; max-width: 720px; color: var(--ink-2); font-size: 14px;\n}\n.contact-note b { color: var(--ink); }\n.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.contact-sites a {\n display: inline-flex; align-items: center; min-height: 34px;\n padding: 4px 12px; border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n background: var(--surface); font-family: var(--font-mono); font-size: 11px;\n font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;\n transition: transform 0.12s, box-shadow 0.12s;\n}\n.contact-sites a:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-off-mid); }\n.contact-hub { margin-top: 26px; }\n\n/* — rangée de navigation locale au-dessus du footer commun — */\n.prefooter { border-top: 1.5px solid var(--ink); background: var(--surface-2); padding: 18px 0; }\n.prefooter .container {\n display: flex; flex-wrap: wrap; align-items: center; gap: 6px 20px;\n font-family: var(--font-mono); font-size: 11.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n}\n.prefooter a, .prefooter .flink {\n color: var(--ink-2); text-decoration: none; text-transform: uppercase;\n letter-spacing: 0.08em; font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;\n min-height: var(--touch); display: inline-flex; align-items: center;\n}\n.prefooter a:hover, .prefooter .flink:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }\n.prefooter .baseline { width: 100%; font-family: var(--font-body); font-weight: 400; font-size: 13px; color: var(--ink-2); text-transform: none; letter-spacing: 0; }\n.ka-footer { margin-top: 0; }\n@media (max-width: 760px) { .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } }\n\n/* ============ Juste valeur (fair value) — pastilles & analyse de prix ====== */\n.fv-badge {\n display: inline-flex; align-items: center; gap: 6px;\n border-radius: var(--r-pill); padding: 6px 12px;\n font-size: 12.5px; font-weight: 700; border: 1.5px solid var(--ink);\n white-space: nowrap;\n}\n.fv-badge .fv-ico { font-size: 10px; line-height: 1; }\n.fv-badge .fv-pct { margin-left: 4px; }\n.fv-badge.fv-compact { padding: 2px 8px; font-size: 11px; gap: 4px; margin-left: 8px; vertical-align: 2px; }\n/* états (mêmes jetons que les badges marché existants — daltonien-compatible :\n l'icône ▼ ≈ ▲ et le libellé portent l'information, pas la couleur seule) */\n.fv-badge.deal-good { background: var(--blue-soft); border-color: rgba(28, 92, 65, 0.35); color: var(--blue); }\n.fv-badge.deal-ok { background: var(--surface); color: var(--ink-2); }\n.fv-badge.deal-high { background: var(--orange-soft); border-color: rgba(255, 106, 0, 0.4); color: var(--orange-deep); }\n\n.f-fairvalue .fv-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 12px; flex-wrap: wrap; margin-bottom: 6px;\n}\n.f-fairvalue .fv-value { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--navy); }\n.f-fairvalue .fv-value small { font-size: 12px; font-weight: 500; color: var(--ink-3); }\n.f-fairvalue .fv-range { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.fv-histo { display: block; width: 100%; height: auto; margin: 10px 0 4px; }\n.fv-histo-lbl { font: 700 10px Inter, system-ui, sans-serif; fill: var(--ink); }\n.fv-histo-lbl-fv { fill: var(--accent-deep, #cc5500); }\n.fv-histo-axis { font: 500 9px Inter, system-ui, sans-serif; fill: var(--ink-3); }\n.fv-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12.5px; color: var(--ink-2); margin: 6px 0 4px; }\n.fv-demo { display: flex; flex-wrap: wrap; gap: 8px; }\n.chip.chip-deal.on { background: var(--blue); border-color: var(--blue); color: var(--white); }\n\n/* =============================================================================\n MapSearch — recherche synchronisée liste ↔ carte (search/MapSearch.tsx)\n Une seule vérité partagée : compteur, tri, survols et sélections miroirs.\n============================================================================= */\n\n.ms { position: relative; }\n.ms-list { position: relative; scroll-behavior: smooth; }\n\n.ms-list-head {\n display: flex; align-items: center; justify-content: space-between;\n gap: 10px; flex-wrap: wrap; padding: 2px 2px 4px;\n}\n.ms-count { font-size: 13.5px; color: var(--ink-2); }\n.ms-count b { color: var(--ink); font-size: 15px; }\n.ms-nopos {\n margin-left: 8px; font-size: 11px; color: var(--ink-3);\n border: 1px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 8px; cursor: help;\n}\n.ms-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }\n.ms-sort select {\n border: 1.5px solid var(--ink); border-radius: var(--r-ctl);\n background: var(--surface); padding: 6px 8px; font-size: 12.5px;\n font-weight: 600; color: var(--ink);\n}\n\n.ms-pills { margin: 0 0 4px; }\n\n.ms-refit {\n align-self: flex-start; border: 1.5px solid var(--ink);\n border-radius: var(--r-pill); background: var(--surface);\n padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--ink-2);\n cursor: pointer; box-shadow: var(--shadow-off-soft);\n}\n.ms-refit:hover { background: var(--navy); color: var(--white); border-color: var(--navy); }\n\n/* barre de progression fine — jamais d'écran qui clignote */\n.ms-progress {\n position: sticky; top: 0; z-index: 4; height: 3px; border-radius: 2px;\n background: linear-gradient(90deg, transparent, var(--accent), transparent);\n background-size: 200% 100%; animation: ms-progress 1s linear infinite;\n}\n@keyframes ms-progress { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n.ms-empty {\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-card);\n padding: 26px 18px; text-align: center; color: var(--ink-2);\n}\n.ms-empty .big { margin-bottom: 6px; color: var(--ink-3); }\n.ms-empty h3 { margin: 0 0 6px; font-size: 16px; color: var(--ink); }\n.ms-empty p { margin: 0 0 14px; font-size: 13px; }\n.ms-empty-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n/* miroirs survol / sélection venus de la carte */\n.map-card { transition: transform 0.15s ease, box-shadow 0.15s ease; }\n.map-card-hover {\n outline: 2px solid var(--accent); outline-offset: 2px;\n transform: translateY(-1px);\n}\n.map-card-pulse { animation: ms-pulse 0.9s ease 1; }\n@keyframes ms-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }\n 70% { box-shadow: 0 0 0 12px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* indicateur discret : l'annonce survolée sur la carte est hors écran */\n.ms-hover-hint {\n position: sticky; z-index: 5; align-self: center;\n background: var(--navy); color: var(--white);\n border-radius: var(--r-pill); padding: 5px 12px;\n font-size: 11.5px; font-weight: 600; pointer-events: none;\n box-shadow: var(--shadow-off-soft);\n}\n.ms-hover-hint.haut { top: 8px; order: -1; }\n.ms-hover-hint.bas { bottom: 8px; }\n\n/* ---------------- carrousel mobile synchronisé aux marqueurs ------------- */\n.ms-carousel {\n position: absolute; left: 0; right: 0; bottom: 10px; z-index: 12;\n display: flex; gap: 10px; overflow-x: auto; padding: 4px 12px;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n.ms-carousel::-webkit-scrollbar { display: none; }\n.ms-car-item {\n flex: 0 0 78%; max-width: 340px; scroll-snap-align: center;\n display: grid; grid-template-columns: 108px 1fr; gap: 0;\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-card); overflow: hidden;\n box-shadow: var(--shadow-off-soft); text-decoration: none; color: inherit;\n}\n.ms-car-item.sel { outline: 2.5px solid var(--accent); outline-offset: 1px; }\n.ms-car-item img, .ms-car-item .smart-img, .ms-car-item .card-fallback {\n width: 108px; height: 96px; object-fit: cover; display: block;\n}\n.ms-car-body { padding: 9px 12px; min-width: 0; }\n.ms-car-price { font-size: 14.5px; font-weight: 700; color: var(--ink); display: flex; gap: 6px; align-items: center; }\n.ms-car-title {\n font-size: 12px; color: var(--ink-2); margin-top: 2px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.ms-car-meta { font-size: 11px; color: var(--ink-3); margin-top: 3px; }\n.ms-car-more {\n flex: 0 0 auto; scroll-snap-align: center; align-self: stretch;\n border: 1.5px solid var(--ink); border-radius: var(--r-card);\n background: var(--navy); color: var(--white);\n font-size: 12.5px; font-weight: 600; padding: 0 18px; cursor: pointer;\n}\n\n/* mobile : carte plein cadre, liste masquée, carrousel par-dessus la carte */\n@media (max-width: 780px) {\n .ms { height: calc(100dvh - 190px); }\n .ms-list { display: none; }\n .ms .mapview { height: 100%; }\n .ka-search-area { top: 8px; }\n}\n@media (min-width: 781px) {\n .ms-carousel { display: none; }\n}\n\n/* mobile : réorganisation du chrome carte autour du carrousel */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 56px; left: 10px; bottom: auto; }\n .ms .ka-loading { bottom: 132px; }\n .ms .ka-empty { transform: translate(-50%, -70%); }\n}\n/* l'auto-défilement vers la carte laisse l'entête de résultats visible */\n.ms { scroll-margin-top: 108px; }\n\n/* mobile, vue carte : chrome sans chevauchement (case en haut, outils sous\n elle, FAB Filtres au-dessus du carrousel) */\n@media (max-width: 780px) {\n .ms .ka-draw-btn { top: 88px; left: 10px; bottom: auto; }\n .ms .ka-locate { top: 88px; right: 10px; }\n .fab-carte { bottom: 148px; }\n}\n\n/* =============================================================================\n KA Scores — pastilles, jauges circulaires, page méthodologie\n============================================================================= */\n.ka-badge {\n display: inline-flex; align-items: center; gap: 4px;\n border: 1.5px solid var(--ink); border-radius: var(--r-pill);\n padding: 1px 8px 1px 3px; font-size: 11px; font-weight: 700;\n color: var(--ink); background: var(--surface); line-height: 1.5;\n vertical-align: middle;\n}\n.ka-badge-logo {\n background: var(--ink); color: var(--white); border-radius: 999px;\n font-size: 8.5px; font-weight: 800; padding: 1.5px 5px; letter-spacing: 0.4px;\n}\n.ka-badge.ka-haut { border-color: var(--green, #256d43); }\n.ka-badge.ka-haut .ka-badge-logo { background: var(--green, #256d43); }\n.ka-badge.ka-bon { border-color: var(--accent); }\n.ka-badge.ka-bon .ka-badge-logo { background: var(--accent); }\n.ka-badge.ka-moyen { opacity: 0.9; }\n.ka-badge.ka-bas { opacity: 0.75; }\n\n.ka-circles, .ka-demo-circles {\n display: flex; gap: 18px; flex-wrap: wrap; margin: 14px 0 6px;\n}\n.ka-circle { text-align: center; width: 92px; }\n.ka-circle-fond {\n fill: none; stroke: var(--line); stroke-width: 6;\n}\n.ka-circle-arc {\n fill: none; stroke: var(--ink-3); stroke-width: 6; stroke-linecap: round;\n transition: stroke-dasharray 0.6s ease;\n}\n.ka-circle.ka-haut .ka-circle-arc { stroke: var(--green, #256d43); }\n.ka-circle.ka-bon .ka-circle-arc { stroke: var(--accent); }\n.ka-circle.ka-moyen .ka-circle-arc { stroke: var(--orange, #d97a2b); }\n.ka-circle.ka-bas .ka-circle-arc { stroke: #b3543f; }\n.ka-circle-val { font: 700 17px var(--font-body); fill: var(--ink); }\n.ka-circle-nom { font-size: 12px; font-weight: 700; color: var(--ink); margin-top: 4px; }\n.ka-circle-label { font-size: 10.5px; color: var(--ink-3); }\n\n.f-kascores h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ka-perso {\n font-size: 12px; font-weight: 600; color: var(--ink-2);\n border: 1.5px dashed var(--line-strong); border-radius: var(--r-pill);\n padding: 2px 10px;\n}\n.ka-detail summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n padding: 6px 0;\n}\n.ka-detail-grille {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px; padding: 8px 0 4px;\n}\n.ka-detail-grille h4 { margin: 0 0 4px; font-size: 12px; color: var(--ink); }\n.ka-detail-grille ul { margin: 0 0 10px; padding-left: 16px; font-size: 12.5px; color: var(--ink-2); }\n.ka-detail-grille li { margin: 2px 0; }\n\n.ka-poids { display: grid; gap: 10px; margin: 14px 0; max-width: 460px; }\n.ka-poids-row {\n display: grid; grid-template-columns: 90px 1fr 48px; align-items: center;\n gap: 12px; font-size: 13px; font-weight: 600; color: var(--ink);\n}\n.ka-poids-row input[type=\"range\"] { accent-color: var(--accent); }\n.ka-poids-row b { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ms-car-price .ka-badge { margin-left: 2px; }\n@media (max-width: 780px) {\n .ms .ka-3d { top: 148px; right: 10px; }\n}\n\n\n/* ---- Registre des loyers (fiche) — bloc citoyen registre-des-loyers.ca ---- */\n.f-rdl .rdl-resume { margin: 2px 0 6px; }\n.f-rdl .rdl-ecart { margin: 0 0 8px; }\n.rdl-rooms { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 10px; }\n.rdl-room { font-size: 13px; color: var(--ink-2); white-space: nowrap; }\n.rdl-room i { font-style: normal; color: var(--ink-3); }\n.rdl-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.rdl-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line, #e6e4df);\n vertical-align: top; }\n.rdl-table .rdl-addr { max-width: 46%; }\n.rdl-table .rdl-prix { font-weight: 600; white-space: nowrap; }\n.rdl-table .rdl-date { white-space: nowrap; color: var(--ink-2); }\n.rdl-table .rdl-dist { color: var(--ink-3); white-space: nowrap; text-align: right; }\n.rdl-room-on { background: var(--sand, #f4efe7); border-radius: 6px; padding: 2px 8px; }\n\n\n/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */\n.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }\n.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;\n font-size: 12.5px; font-weight: 700; }\n.zi-eleve { background: #fde8e8; color: #a12622; }\n.zi-modere { background: #fdf3e0; color: #8a5a00; }\n.zi-present { background: #fdf3e0; color: #8a5a00; }\n.zi-ok { background: #e7f4ea; color: #1e6b34; }\n.zi-nc { background: var(--sand, #f0ede8); color: var(--ink-3); }\n.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }\n\n\n/* ---- Historique TAL (fiche) — décisions SOQUIJ à l'adresse ---- */\n.f-tal .tal-pending, .f-tal .tal-ok, .f-tal .tal-resume { margin: 2px 0 8px; }\n.tal-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }\n.tal-liste li { padding: 8px 0; border-top: 1px solid var(--line, #e6e4df); }\n.tal-ligne { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }\n.tal-date { font-weight: 600; color: var(--ink-2); }\n.tal-part { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: var(--sand, #f0ede8); color: var(--ink-2); }\n.tal-part-locateur { background: #fdf3e0; color: #8a5a00; }\n.tal-verdict { font-size: 12px; color: var(--ink-3); }\n.tal-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 5px 0 3px; }\n.tal-tag { font-size: 12px; padding: 1px 8px; border-radius: 999px;\n background: #fde8e8; color: #a12622; }\n.tal-ref { font-size: 12.5px; color: var(--ink-3); }\n\n\n/* ---- Registre des loyers v2 : tuiles, bande de position, barres ---- */\n.rdl-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 12px; }\n.rdl-kpi { flex: 1 1 90px; min-width: 90px; background: var(--surface-2, #f7f5f1);\n border-radius: 10px; padding: 10px 12px; }\n.rdl-kpi-v { display: block; font-size: 20px; font-weight: 700;\n letter-spacing: -0.02em; color: var(--navy); }\n.rdl-kpi-l { display: block; font-size: 11px; line-height: 1.35;\n color: var(--ink-3); margin-top: 2px; }\n.rdl-strip { width: 100%; height: auto; display: block; margin: 2px 0 4px; }\n.rdl-strip-track { fill: var(--surface-2, #f0ede8); }\n.rdl-strip-box { fill: var(--accent-soft, #fff1e6);\n stroke: var(--accent, #ff6a00); stroke-width: 1; }\n.rdl-strip-med { stroke: var(--ink-2, #4c4a45); stroke-width: 2; }\n.rdl-strip-me { stroke: var(--accent-deep, #cc5500); stroke-width: 2; }\n.rdl-strip-me-dot { fill: var(--accent-deep, #cc5500);\n stroke: var(--surface, #fff); stroke-width: 2; }\n.rdl-strip-lbl, .rdl-strip-axis { font-size: 10.5px; fill: var(--ink-3); }\n.rdl-strip-me-lbl { font-size: 11px; font-weight: 700;\n fill: var(--accent-deep, #cc5500); }\n.rdl-bars { width: 100%; height: auto; display: block; margin: 6px 0 2px; }\n.rdl-bar { fill: var(--accent, #ff6a00); opacity: 0.55; }\n.rdl-bar-on { fill: var(--accent-deep, #cc5500); opacity: 1; }\n.rdl-bar-val { font-size: 10.5px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat { font-size: 11px; fill: var(--ink-2, #4c4a45); }\n.rdl-bar-cat-on { font-weight: 700; fill: var(--ink, #1c1b18); }\n.rdl-bar-n { font-size: 9.5px; fill: var(--ink-3, #8a877f); }\n.rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }\n.rdl-ecart { margin: 4px 0 8px; }\n.rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }\n\n\n/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */\n.q-crime { width: 100%; }\n.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }\n.q-crime-cats li { display: flex; align-items: center; gap: 8px;\n padding: 3px 0; font-size: 12.5px; }\n.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.q-crime-barre i { display: block; height: 100%; border-radius: 5px;\n background: var(--accent, #ff6a00); opacity: 0.6; }\n.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; }\n.q-crime-n small { font-weight: 400; color: var(--ink-3); }\n.q-crime-src { margin-top: 8px; }\n\n\n/* ---- Qualité de l'air (RSQAQ) + Essence à proximité ---- */\n.air-liste { list-style: none; margin: 10px 0 0; padding: 0; }\n.air-liste li { display: flex; align-items: center; gap: 8px;\n padding: 4px 0; font-size: 12.5px; }\n.air-nom { flex: 0 0 44%; color: var(--ink-2); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.air-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);\n border-radius: 5px; overflow: hidden; }\n.air-barre i { display: block; height: 100%; border-radius: 5px;\n background: #4d9e64; }\n.air-barre i.air-sur { background: #c96a1f; }\n.air-val { flex: 0 0 34%; text-align: right; font-weight: 600;\n color: var(--ink); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.air-val small { font-weight: 400; color: var(--ink-3); }\n.air-val .air-ref { display: block; font-size: 10px; }\n.gaz-head th { text-align: left; font-size: 11px; color: var(--ink-3);\n font-weight: 600; padding: 2px 8px 2px 0; }\n.gaz-best { display: inline-block; margin-left: 6px; padding: 1px 7px;\n border-radius: 999px; background: #e7f4ea; color: #1e6b34;\n font-size: 10.5px; font-weight: 700; }\n.gaz-adr { display: block; font-size: 11px; color: var(--ink-3); }\n\n\n/* ---- Commerces et transport (fiche) ---- */\n.cm-grille { list-style: none; margin: 6px 0 0; padding: 0;\n display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }\n@media (max-width: 560px) { .cm-grille { grid-template-columns: 1fr; } }\n.cm-item { display: flex; align-items: center; gap: 9px; padding: 5px 0;\n border-bottom: 1px solid var(--line, #eeece7); min-width: 0; }\n.cm-ico { flex: 0 0 28px; }\n.cm-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.cm-nom { font-size: 13px; font-weight: 600; color: var(--ink, #222);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-poi { font-size: 11px; color: var(--ink-3, #8a877f);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cm-dist { flex: 0 0 auto; font-size: 13px; font-weight: 700;\n color: var(--ink-2, #4c4a45); white-space: nowrap; }\n\n\n/* ---- Coût d'électricité (Hydro-Québec) ---- */\n.hydro-intro { margin: 2px 0 12px; color: var(--ink-2, #4c4a45); }\n.hydro-btn { appearance: none; border: 0; border-radius: 10px; cursor: pointer;\n background: var(--accent, #2b7a3b); color: #fff; font-weight: 700;\n font-size: 14px; padding: 11px 18px; }\n.hydro-btn:disabled { opacity: 0.6; cursor: progress; }\n.hydro-montant { font-size: 26px; font-weight: 800; letter-spacing: -0.02em;\n color: var(--navy, #1c1b18); display: flex; align-items: baseline;\n gap: 10px; flex-wrap: wrap; }\n.hydro-montant small { font-size: 14px; font-weight: 500; color: var(--ink-3); }\n.hydro-an { font-size: 14px; font-weight: 600; color: var(--ink-2, #4c4a45); }\n.hydro-vide { color: var(--ink-3, #8a877f); }\n\n\n/* ============================================================================\n PASSE DE DESIGN v3 — « éditorial sharp » appliqué à toute la fiche\n Blocs = cartes encre (bordure 1,5 px + ombre décalée douce), titres à\n marqueur accent + filet, tuiles KPI blanches à ombre dure, tableaux zébrés,\n barres en dégradé, pastilles unifiées (mono, point d'état, liseré).\n Aucune propriété `order` (règle Groupe Ka : ordre DOM = ordre visuel).\n ========================================================================== */\n\n/* --- Blocs de la fiche : cartes posées sur le papier --- */\n.f-bloc:not(.f-galerie):not(.f-hero) {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-card);\n padding: 18px 20px 16px;\n box-shadow: var(--shadow-off-soft);\n}\n@media (max-width: 640px) {\n .f-bloc:not(.f-galerie):not(.f-hero) { padding: 15px 15px 13px; }\n}\n\n/* --- Titres de section : carré accent + filet éditorial --- */\n.f-bloc h2, .quartier h2 {\n display: flex; align-items: center; gap: 10px;\n font-size: 19px; letter-spacing: -0.015em; margin-bottom: 14px;\n}\n.f-bloc h2::before, .quartier h2::before {\n content: \"\"; flex: 0 0 9px; width: 9px; height: 9px;\n background: var(--accent); border: 1.5px solid var(--ink);\n box-shadow: 2px 2px 0 var(--ink);\n}\n.f-bloc h2::after, .quartier h2::after {\n content: \"\"; flex: 1 1 auto; height: 1.5px; min-width: 20px;\n background: var(--line);\n}\n\n/* --- Tuiles KPI : blanches, bord encre, ombre dure accent --- */\n.rdl-kpi, .kv .cell {\n background: var(--surface);\n border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl);\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.rdl-kpi-v { font-family: var(--font-display); }\n.q-cell { box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); border-radius: var(--r-ctl); }\n.q-val { font-size: 20px; }\n\n/* --- Tableaux : en-têtes mono, zébrure, survol accent --- */\n.rdl-table td { padding: 7px 8px 7px 0; }\n.rdl-table tbody tr:nth-child(even) td,\n.rdl-table > tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.rdl-table tr:hover td { background: var(--accent-soft, #fff1e6); }\n.gaz-head th {\n font-family: var(--font-mono); font-size: 10px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.08em;\n border-bottom: 1.5px solid var(--ink);\n}\n.rdl-cap { font-family: var(--font-display); }\n\n/* --- Barres : pistes bordées + remplissage en dégradé --- */\n.q-bar-track, .q-crime-barre, .air-barre {\n background: var(--surface-2, #faf9f5);\n border: 1px solid var(--line);\n height: 12px;\n}\n.q-bar-fill {\n background: linear-gradient(90deg, var(--accent, #ff6a00), var(--accent-deep, #cc5500));\n}\n.q-crime-barre i {\n background: linear-gradient(90deg, #ff9a52, var(--accent, #ff6a00));\n opacity: 0.9;\n}\n.air-barre i { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.air-barre i.air-sur { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n\n/* --- Pastilles d'état unifiées (zi-badge & cie) : mono + point d'état --- */\n.zi-badge {\n display: inline-flex; align-items: center; gap: 6px;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 4px 10px; border: 1.5px solid currentColor;\n}\n.zi-badge::before {\n content: \"\"; width: 6px; height: 6px; border-radius: 50%;\n background: currentColor;\n}\n.gaz-best { border: 1px solid currentColor; }\n\n/* --- Historique TAL : chaque décision devient une mini-carte --- */\n.tal-liste { display: flex; flex-direction: column; gap: 8px; }\n.tal-liste li {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-top: 1px solid var(--line);\n border-radius: var(--r-ctl); padding: 9px 12px;\n background: var(--surface-2, #faf9f5);\n}\n.tal-part, .tal-tag {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px;\n}\n.tal-ref { font-family: var(--font-mono); font-size: 11px; }\n\n/* --- Notes de bas de bloc : filet pointillé --- */\n.f-bloc .fine, .quartier .fine {\n margin-top: 12px; padding-top: 9px;\n border-top: 1px dashed var(--line);\n}\n\n/* --- Listes à liseré (inondation) alignées sur le reste --- */\n.zi-liste li { padding: 6px 0; }\n\n/* ============================================================================\n Dossier d'intelligence locative — Coût réel, Historique Lou-Ka, Immeuble,\n Gestionnaire, Vie quotidienne en hiver\n ========================================================================== */\n\n/* --- Pastilles de statut de donnée (observed / estimated / …) --- */\n.st-pill {\n display: inline-block;\n font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em;\n padding: 2px 7px; border: 1.5px solid currentColor; border-radius: 3px;\n white-space: nowrap; vertical-align: 1px;\n}\n.cr-poste .st-pill { margin-left: 8px; }\n.st-included { color: #2e7d4a; background: #eef7f0; }\n.st-observed { color: var(--ink); background: var(--surface); }\n.st-estimated { color: var(--accent-deep, #cc5500); background: var(--accent-soft, #fff1e6); }\n.st-unknown { color: var(--ink-3); background: var(--surface-2, #faf9f5); border-style: dashed; }\n\n/* --- Coût réel mensuel --- */\n.cr-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }\n.cr-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }\n.cr-table tbody tr:nth-child(even) td { background: var(--surface-2, #faf9f5); }\n.cr-table tbody tr:hover td { background: var(--accent-soft, #fff1e6); }\n.cr-table tfoot tr:first-child td { background: var(--accent-soft, #fff1e6); }\n.cr-poste { color: var(--ink-2); }\n.cr-montant {\n text-align: right; font-family: var(--font-mono); font-weight: 600;\n white-space: nowrap;\n}\n.cr-table tfoot td {\n border-bottom: none; border-top: 1.5px solid var(--ink);\n font-weight: 700; padding-top: 9px;\n}\n.cr-annuel { color: var(--ink-3); font-weight: 500; font-size: 12px; }\n.cr-inconnus {\n margin: 10px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--ink-2);\n background: var(--accent-soft, #fff1e6);\n border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl);\n}\n.cr-pi2 { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.cr-pi2 b { font-family: var(--font-mono); }\n\n/* --- Historique Lou-Ka : timeline --- */\n.hl-timeline {\n list-style: none; margin: 12px 0 0; padding: 0 0 0 14px;\n border-left: 2px solid var(--ink);\n display: flex; flex-direction: column; gap: 8px;\n}\n.hl-item { position: relative; font-size: 13px; color: var(--ink-2); }\n.hl-item::before {\n content: \"\"; position: absolute; left: -19.5px; top: 5px;\n width: 9px; height: 9px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--ink-3);\n}\n.hl-item.hl-prix::before { border-color: var(--accent, #ff6a00); background: var(--accent-soft, #fff1e6); }\n.hl-item.hl-disparition::before { border-color: #b3552e; background: #fdeee7; }\n.hl-item.hl-reapparition::before { border-color: #2e7d4a; background: #eef7f0; }\n.hl-date {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3);\n display: inline-block; min-width: 92px;\n}\n.hl-texte b { font-family: var(--font-mono); }\n\n/* --- Vies antérieures (annonces recyclées) --- */\n.hl-recyclees { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }\n.hl-recyclees h3 {\n margin: 0 0 8px; font-size: 13px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);\n}\n.hl-match {\n border: 1px solid var(--line); border-left: 3px solid var(--accent, #ff6a00);\n border-radius: var(--r-ctl); background: var(--surface-2, #faf9f5);\n padding: 8px 12px; margin: 0 0 8px; font-size: 13px;\n}\n.hl-signaux {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3); letter-spacing: 0.02em;\n}\n\n/* --- Passeport de l'immeuble --- */\n.im-parcc { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }\n.im-indicateurs { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }\n.im-indic { font-size: 13px; }\n.im-indic > .k {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n display: inline-block; margin-right: 4px;\n}\n.im-detail { color: var(--ink-2); font-size: 12.5px; }\n\n/* --- Qui gère ce logement? --- */\n.gest-head { margin-bottom: 10px; }\n.gest-nom { font-family: var(--font-display); font-weight: 700; font-size: 17px; }\n.gest-meta { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }\n.gest-google { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }\n.gest-note {\n font-family: var(--font-display); font-weight: 700; font-size: 20px;\n color: var(--accent-deep, #cc5500);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 5px 10px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n white-space: nowrap;\n}\n.gest-navis { font-size: 12.5px; color: var(--ink-2); }\n.gest-bars { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }\n.gest-bar { display: flex; align-items: center; gap: 8px; }\n.gb-n {\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n color: var(--ink-2); min-width: 26px; text-align: right;\n}\n.gb-track {\n flex: 1 1 auto; height: 12px;\n background: var(--surface-2, #faf9f5); border: 1px solid var(--line);\n overflow: hidden; display: block;\n}\n.gb-fill { display: block; height: 100%; background: var(--ink-3); min-width: 0; }\n.gb-fill.pos { background: linear-gradient(90deg, #6fbc85, #2e7d4a); }\n.gb-fill.neg { background: linear-gradient(90deg, #e08a48, #c96a1f); }\n.gb-c {\n font-family: var(--font-mono); font-size: 11px; color: var(--ink-3);\n min-width: 24px;\n}\n.gest-stats {\n display: flex; flex-wrap: wrap; align-items: center; gap: 10px;\n font-size: 13px; color: var(--ink-2); margin: 8px 0;\n}\n.gest-themes { font-size: 13px; margin: 8px 0; }\n.gest-themes > .k { color: var(--ink-2); }\n.gest-themes .zi-badge { margin: 2px 4px 2px 0; }\n.gest-avis { margin-top: 10px; }\n.gest-avis summary {\n cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-2);\n}\n.gest-citation {\n margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--ink-2);\n border-left: 3px solid var(--line); background: var(--surface-2, #faf9f5);\n border-radius: var(--r-ctl);\n}\n.gest-citation footer {\n margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px;\n color: var(--ink-3);\n}\n\n/* --- Vie quotidienne en hiver --- */\n.hiver-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n.hiver-score {\n font-family: var(--font-display); font-weight: 700; font-size: 30px;\n line-height: 1; color: var(--ink);\n background: var(--surface); border: 1.5px solid var(--ink);\n border-radius: var(--r-ctl); padding: 8px 12px;\n box-shadow: 3px 3px 0 var(--accent-soft, #fff1e6);\n}\n.hiver-sur { font-size: 12.5px; color: var(--ink-3); }\n.hiver-detail {\n list-style: none; margin: 12px 0 0; padding: 0;\n display: flex; flex-direction: column; gap: 6px;\n}\n.hiver-detail li {\n display: flex; align-items: baseline; gap: 8px;\n font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--line);\n}\n.hiver-detail li:last-child { border-bottom: none; }\n.hd-crit {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3);\n min-width: 110px;\n}\n.hd-val { flex: 1 1 auto; color: var(--ink-2); }\n.hd-score {\n font-family: var(--font-mono); font-weight: 700; font-size: 12px;\n min-width: 28px; text-align: right;\n}\n","structuredPatch":[{"oldStart":2289,"oldLines":3,"newStart":2289,"newLines":24,"lines":[" font-family: var(--font-mono); font-weight: 700; font-size: 12px;"," min-width: 28px; text-align: right;"," }","+","+/* --- Adaptatif mobile : dossier d'intelligence locative --- */","+@media (max-width: 640px) {","+ .cr-table { font-size: 12.5px; }","+ .cr-table td { padding: 6px 6px 6px 0; }","+ .cr-poste .st-pill { margin-left: 6px; }","+ .cr-montant { font-size: 12.5px; }","+ .hl-date { display: block; min-width: 0; margin-bottom: 2px; }","+ .hl-item { font-size: 12.5px; }","+ .hl-match { font-size: 12.5px; }","+ .im-indic .im-detail { display: block; margin-top: 3px; }","+ .gest-nom { font-size: 16px; }","+ .gest-stats { font-size: 12.5px; gap: 8px; }","+ .gest-citation { font-size: 12.5px; }","+ .gb-n { min-width: 24px; font-size: 10.5px; }","+ .gb-c { min-width: 20px; font-size: 10.5px; }","+ .hiver-score { font-size: 26px; padding: 6px 10px; }","+ .hiver-sur { flex-basis: 100%; }","+ .hd-crit { min-width: 86px; font-size: 10px; }","+ .hiver-detail li { font-size: 12.5px; }","+}"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"0c3115c1-4f17-4807-8070-1a0a681c98e5","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMfviRs3ckv2eBdKENg","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS9wIKpwEIERgCKkAbfGVKwyyA8EMHacDWxjN443amM5p7WfrfUUGAXWsorBuv0klT4G5NTCccK1Iluvxe3eZWzF2mQOnHawyVIarZMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGjibPUBrABAhIMkpR/T0rfEzxhZNqwGgxF+8e11mTp2YHH7/4iMFaNX5+VKCG4Wc7iHE2GeP8ncIWaOxgZkRmrS5H4vethqhjjsnBv0yIIi4zMqG5hMyp9peVH584+amGXfBMUXV/cSmOdiYcshmYjU2CFUiAwq6cpg+fawZ98TOLzMVReJoxelXM8n4pNeWehwnmFvc7E4fj2LEDc9fReBNh4xewaCf9A4kBZgKCMtl4Vv9R6GrbsmWqWWyHnDmxR53b5GqdZZGax+L1DFe3WHa+Xn2kYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1686,"cache_read_input_tokens":110147,"cache_creation":{"ephemeral_5m_input_tokens":1686,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"32b1f47d-eacb-4c7e-8d47-7d565d40c565"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMfviRs3ckv2eBdKENg","type":"message","role":"assistant","content":[{"type":"text","text":"CSS terminé. Je passe au PDF — je vérifie d'abord les signatures exactes des modules dans web.py."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1686,"cache_read_input_tokens":110147,"cache_creation":{"ephemeral_5m_input_tokens":1686,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1239a1fd-b6fc-44ee-b52f-80845cb08f55"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMfviRs3ckv2eBdKENg","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0154q7tUo6M9PDCmN8hPRBHk","name":"TodoWrite","input":{"todos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"in_progress","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"pending","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1686,"cache_read_input_tokens":110147,"cache_creation":{"ephemeral_5m_input_tokens":1686,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b1245fb9-d2d8-4539-8801-cd13b547596a"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0154q7tUo6M9PDCmN8hPRBHk","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f0ed124f-0fba-451b-90e6-c9a4b43dfb80","timestamp":"2026-08-24T22:24:38.530Z","tool_use_result":{"oldTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"in_progress","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"pending","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"pending","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}],"newTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"in_progress","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"pending","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMfviRs3ckv2eBdKENg","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VZcMSbu3By19MaiMd77a2i","name":"Bash","input":{"command":"grep -n \"historique\\|cout-reel\\|recyclees\\|immeuble\\|managers\\|hiver\\|building\" louka/web.py | head -20; echo \"---signatures---\"; grep -n \"^def \\|^ return\" louka/history.py louka/coutreel.py louka/recycled.py louka/building.py louka/hiver.py louka/managers.py | grep \"^.*def \" | head","description":"Get module function signatures"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1686,"cache_read_input_tokens":110147,"cache_creation":{"ephemeral_5m_input_tokens":1686,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f3b09e9c-1984-4a66-9015-4b26354df75c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VZcMSbu3By19MaiMd77a2i","type":"tool_result","content":"613:@app.get(\"/api/listings/{uid}/historique\")\n624:@app.get(\"/api/listings/{uid}/cout-reel\")\n635:@app.get(\"/api/listings/{uid}/recyclees\")\n643:@app.get(\"/api/immeuble\")\n644:def immeuble_fiche(bkey: str):\n645: \"\"\"Passeport de l'immeuble (stats précalculées par building.rollup).\"\"\"\n646: from . import building\n647: out = building.fiche(bkey)\n653:@app.get(\"/api/managers/{source_id}\")\n656: base — aucun appel SerpApi au chargement de page (louka/managers.py).\"\"\"\n657: from . import managers\n658: out = managers.fiche(source_id)\n676: # commodités de proximité (cache par immeuble, voir louka/poi.py)\n684: # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n704: # passeport de l'immeuble (stats précalculées) + score hiver\n705: if d.get(\"building_key\"):\n706: from . import building\n707: d[\"immeuble\"] = building.fiche(d[\"building_key\"], con)\n709: d[\"immeuble\"] = None\n710: from . import hiver\n---signatures---\nlouka/history.py:19:def timeline(uid: str, con=None) -> dict | None:\nlouka/coutreel.py:28:def _pctile(values: list[float], x: float) -> int:\nlouka/coutreel.py:33:def pi2(con, l: dict) -> dict | None:\nlouka/coutreel.py:73:def compute(uid: str, con=None) -> dict | None:\nlouka/recycled.py:33:def _tokens(text: str | None) -> set[str]:\nlouka/recycled.py:37:def _jaccard(a: set[str], b: set[str]) -> float:\nlouka/recycled.py:43:def _sha1_set(con, images_json: str | None) -> set[str]:\nlouka/recycled.py:56:def _unit_of(address: str | None, city: str | None) -> str | None:\nlouka/recycled.py:61:def _score(con, cur: dict, cand: dict) -> tuple[int, list[str]]:\nlouka/recycled.py:114:def lookup(uid: str, con=None) -> dict:","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"079970b5-6188-40b6-961c-d1913a0cadf9","timestamp":"2026-08-24T22:24:40.000Z","tool_use_result":{"stdout":"613:@app.get(\"/api/listings/{uid}/historique\")\n624:@app.get(\"/api/listings/{uid}/cout-reel\")\n635:@app.get(\"/api/listings/{uid}/recyclees\")\n643:@app.get(\"/api/immeuble\")\n644:def immeuble_fiche(bkey: str):\n645: \"\"\"Passeport de l'immeuble (stats précalculées par building.rollup).\"\"\"\n646: from . import building\n647: out = building.fiche(bkey)\n653:@app.get(\"/api/managers/{source_id}\")\n656: base — aucun appel SerpApi au chargement de page (louka/managers.py).\"\"\"\n657: from . import managers\n658: out = managers.fiche(source_id)\n676: # commodités de proximité (cache par immeuble, voir louka/poi.py)\n684: # KA Scores de l'immeuble (Walk/Transit/Bike/Calme/Services + global)\n704: # passeport de l'immeuble (stats précalculées) + score hiver\n705: if d.get(\"building_key\"):\n706: from . import building\n707: d[\"immeuble\"] = building.fiche(d[\"building_key\"], con)\n709: d[\"immeuble\"] = None\n710: from . import hiver\n---signatures---\nlouka/history.py:19:def timeline(uid: str, con=None) -> dict | None:\nlouka/coutreel.py:28:def _pctile(values: list[float], x: float) -> int:\nlouka/coutreel.py:33:def pi2(con, l: dict) -> dict | None:\nlouka/coutreel.py:73:def compute(uid: str, con=None) -> dict | None:\nlouka/recycled.py:33:def _tokens(text: str | None) -> set[str]:\nlouka/recycled.py:37:def _jaccard(a: set[str], b: set[str]) -> float:\nlouka/recycled.py:43:def _sha1_set(con, images_json: str | None) -> set[str]:\nlouka/recycled.py:56:def _unit_of(address: str | None, city: str | None) -> str | None:\nlouka/recycled.py:61:def _score(con, cur: dict, cand: dict) -> tuple[int, list[str]]:\nlouka/recycled.py:114:def lookup(uid: str, con=None) -> dict:","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"ec52b9f0-353e-4635-8f99-a2b3fa85d4d5","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMgd6JNxD9Vb5aCRJha","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FYUMJFVCbPmfqniQPF1YXm","name":"Bash","input":{"command":"grep -n \"^def \" louka/building.py louka/hiver.py louka/managers.py","description":"Get building/hiver/managers public functions"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1479,"cache_read_input_tokens":111833,"cache_creation":{"ephemeral_5m_input_tokens":1479,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3811d757-c5c8-4b0b-be0d-91c43e8661be"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FYUMJFVCbPmfqniQPF1YXm","type":"tool_result","content":"louka/building.py:35:def building_key(address: str | None, city: str | None,\nlouka/building.py:46:def _median(vals: list[float]) -> float | None:\nlouka/building.py:51:def _stats_for(members: list[dict], now: float) -> dict:\nlouka/building.py:149:def rollup(con=None) -> dict:\nlouka/building.py:222:def fiche(bkey: str, con=None) -> dict | None:\nlouka/hiver.py:38:def _sous_score(dist_m: float, plein: float, nul: float) -> float:\nlouka/hiver.py:47:def score(lat: float | None, lng: float | None, con=None) -> dict | None:\nlouka/managers.py:54:def _norm_name(s: str | None) -> str:\nlouka/managers.py:62:def _domain(url: str | None) -> str | None:\nlouka/managers.py:71:def seed(con=None) -> dict:\nlouka/managers.py:107:def _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\nlouka/managers.py:133:def resolve(manager_id: int, con=None) -> dict:\nlouka/managers.py:204:def _content_hash(author: str | None, rating, text: str | None) -> str:\nlouka/managers.py:209:def sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\nlouka/managers.py:317:def _analyze_review(rating, text: str | None) -> dict:\nlouka/managers.py:343:def _aggregate(con, manager_id: int) -> dict:\nlouka/managers.py:406:def _iso_ts(iso: str | None) -> float | None:\nlouka/managers.py:417:def fiche(source_id: str, con=None) -> dict | None:\nlouka/managers.py:480:def precompute(budget_s: float = 120.0, con=None) -> dict:","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0f39fb39-671b-4665-be2b-e7d4983bcbc2","timestamp":"2026-08-24T22:24:48.967Z","tool_use_result":{"stdout":"louka/building.py:35:def building_key(address: str | None, city: str | None,\nlouka/building.py:46:def _median(vals: list[float]) -> float | None:\nlouka/building.py:51:def _stats_for(members: list[dict], now: float) -> dict:\nlouka/building.py:149:def rollup(con=None) -> dict:\nlouka/building.py:222:def fiche(bkey: str, con=None) -> dict | None:\nlouka/hiver.py:38:def _sous_score(dist_m: float, plein: float, nul: float) -> float:\nlouka/hiver.py:47:def score(lat: float | None, lng: float | None, con=None) -> dict | None:\nlouka/managers.py:54:def _norm_name(s: str | None) -> str:\nlouka/managers.py:62:def _domain(url: str | None) -> str | None:\nlouka/managers.py:71:def seed(con=None) -> dict:\nlouka/managers.py:107:def _score_candidate(mgr: dict, cand: dict) -> tuple[float, dict]:\nlouka/managers.py:133:def resolve(manager_id: int, con=None) -> dict:\nlouka/managers.py:204:def _content_hash(author: str | None, rating, text: str | None) -> str:\nlouka/managers.py:209:def sync_reviews(manager_id: int, con=None, max_pages: int = MAX_PAGES) -> dict:\nlouka/managers.py:317:def _analyze_review(rating, text: str | None) -> dict:\nlouka/managers.py:343:def _aggregate(con, manager_id: int) -> dict:\nlouka/managers.py:406:def _iso_ts(iso: str | None) -> float | None:\nlouka/managers.py:417:def fiche(source_id: str, con=None) -> dict | None:\nlouka/managers.py:480:def precompute(budget_s: float = 120.0, con=None) -> dict:","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"22631b68-90f4-439c-a46f-e96b0b851070","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMhHPus52gA8C5dTQqX","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"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"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":747,"cache_read_input_tokens":113312,"cache_creation":{"ephemeral_5m_input_tokens":747,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fab97045-3ecd-4531-a256-ec72a71e50a5"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMhHPus52gA8C5dTQqX","type":"message","role":"assistant","content":[{"type":"text","text":"Signatures confirmées. J'ajoute les cinq sections au PDF de la fiche — d'abord les appels de données, puis un helper de date."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":747,"cache_read_input_tokens":113312,"cache_creation":{"ephemeral_5m_input_tokens":747,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"31dc3722-f10d-41f7-9617-f222835d59a5"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMhHPus52gA8C5dTQqX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011uhJrEiGrJMr5uFXXy4jwd","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)","new_string":" hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":747,"cache_read_input_tokens":113312,"cache_creation":{"ephemeral_5m_input_tokens":747,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"52524387-fb33-4329-b0e2-c54fb72a16cd"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011uhJrEiGrJMr5uFXXy4jwd","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6a0f0c70-11d3-441e-a1aa-ed571be98655","timestamp":"2026-08-24T22:25:37.393Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)","newString":" hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":320,"oldLines":6,"newStart":320,"newLines":20,"lines":[" com = _safe(com_mod.nearby, lat, lng) if geo else None"," hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\","," uid=uid, lat=lat, lng=lng, solve=False)","+ # dossier d'intelligence locative (mêmes modules que les blocs du site)","+ from . import building as bld_mod","+ from . import coutreel as cr_mod","+ from . import history as hist_mod","+ from . import hiver as hiv_mod","+ from . import managers as mgr_mod","+ from . import recycled as rec_mod","+ cout = _safe(cr_mod.compute, uid)","+ histo = _safe(hist_mod.timeline, uid)","+ recyc = _safe(rec_mod.lookup, uid)","+ imm = (_safe(bld_mod.fiche, l[\"building_key\"])","+ if l.get(\"building_key\") else None)","+ hiv = _safe(hiv_mod.score, lat, lng) if geo else None","+ gest = _safe(mgr_mod.fiche, l[\"source\"])"," "," buf = io.BytesIO()"," c = rl_canvas.Canvas(buf, pagesize=letter)"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMhHPus52gA8C5dTQqX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01544Zujr6kBgvzTdw6mDthQ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":"_VERDICTS = {","new_string":"def _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":747,"cache_read_input_tokens":113312,"cache_creation":{"ephemeral_5m_input_tokens":747,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"434547a8-0018-44af-b711-49caa9979ee0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01544Zujr6kBgvzTdw6mDthQ","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a829ee12-79f1-401c-a5f1-99d05a65610d","timestamp":"2026-08-24T22:25:39.540Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":"_VERDICTS = {","newString":"def _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":241,"oldLines":6,"newStart":241,"newLines":15,"lines":[" return d"," "," ","+def _fmt_ts(ts, jour: bool = False) -> str:","+ \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"","+ if not ts:","+ return \"—\"","+ t = time.localtime(ts)","+ mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"","+ return f\"{t.tm_mday} {mois}\" if jour else mois","+","+"," _VERDICTS = {"," \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),"," \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"56d0c729-ad1f-4097-a985-e1b49bed947f","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMm2RreBf6BKmnUtRYv","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISqwUKpwEIERgCKkCtLZAOHYhFsc6gWH8EFwQPVRyno716oPsBxfXDutDqVMOx32w07AHRBd4kSDdbwBTV8exkBeUjLXlti6QE3DzgMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHvibPUBrABAhIM6rEJsBdIO1V5PD/XGgyWtBs2xDNykGh9Lr4iMBFVC7SOS1l7avufWmL7c5NCOtQcNamYOvNeEHKkQyxQq9acqk+w4K9GtMbrl0ihsSqwA3VbV8yy8veHyWUycEsK2/YGe/m/RDlukT26O1oAfXY1lW/vVkDwQR1WUdLP952qRmnFv+AxblN9veQABsotzO1km8IuKwFy+b/rKiHePDcMjfSm9YkfHKnsiIHtnn0ReamF+wVBk9cU+mzyXXFumw1tm1xLAI0Xk28Is9/fmYypy8nwu82O59qwvjssu/XhSJsFiAUldp5iaCwtkvVJOtcvNb+FxLz90Ryjnr2/5wcHkTNhhZhau8zawNHDfd5xMTxQDpTUy6q/fzfTlgXWkD1g3SgUhksYsrk76awXoqhE+KMx9YJbxkKYEr6Rpj+lTTp2mFhVXitVPE1PgIbGacs6tVE5w+Cc4E7kdL19LNkCI0ib7Hjo6/tEUlIU7m7p/WPuhPKCbWvslvK0ArRQ4TDCs+7V6ZW88PTHOFHDkn9GBTBQXMNz9Cax8OTE4TQ0ETwpNWQKkpOqYF1FrQrGmILeCGhErcoFYBWPG6VYx6ZTZUGu+aEEpvdOF3/o/vgSNPZuSn4Wc+vkM8neACiqiAFSFCstQjYNOQBQOvEx81uJiF7G4h8TxpVbPXdl2VEDcBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":4055,"cache_read_input_tokens":114059,"cache_creation":{"ephemeral_5m_input_tokens":4055,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fc421f74-f785-4244-9777-52ee17e96b0b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMm2RreBf6BKmnUtRYv","type":"message","role":"assistant","content":[{"type":"text","text":"Maintenant les quatre sections (coût réel, historique, immeuble, gestionnaire) insérées entre l'analyse de prix et le registre des loyers, comme sur la fiche web."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":4055,"cache_read_input_tokens":114059,"cache_creation":{"ephemeral_5m_input_tokens":4055,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"66514d7e-10f4-4101-9bf1-f46604fb2a3d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMm2RreBf6BKmnUtRYv","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XUjP1Tme5BQBrEz4rmzSks","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" # --- Registre des loyers (déclarations citoyennes à proximité)","new_string":" # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":4055,"cache_read_input_tokens":114059,"cache_creation":{"ephemeral_5m_input_tokens":4055,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ae2e1599-e6fb-4126-be53-86dc121a172c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XUjP1Tme5BQBrEz4rmzSks","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ebaf9d69-98a8-4bf0-894f-7a635e504d3f","timestamp":"2026-08-24T22:26:42.695Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" # --- Registre des loyers (déclarations citoyennes à proximité)","newString":" # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":591,"oldLines":6,"newStart":591,"newLines":277,"lines":[" c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")"," y -= 16"," ","+ # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)","+ if cout and cout.get(\"loyer\") is not None:","+ lignes_cr = cout.get(\"lignes\") or []","+ y = place(y, 58 + len(lignes_cr) * 12 + 46)","+ y = s.titre_section(y, \"Coût réel mensuel\")","+ stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),","+ \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}","+ for li in lignes_cr:","+ lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])","+ c.setFont(\"Helvetica\", 8.5)","+ c.setFillColor(INK2)","+ c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])","+ c.setFont(\"Courier-Bold\", 6.5)","+ c.setFillColor(col)","+ c.drawString(M + 230, y, lab)","+ m_ = li.get(\"montant\")","+ if m_ is not None and m_ > 0:","+ mtx = _fmt_money(m_)","+ elif m_ == 0 and li.get(\"statut\") == \"included\":","+ mtx = f\"0{NBSP}$\"","+ else:","+ mtx = \"—\"","+ c.setFont(\"Helvetica-Bold\", 8.5)","+ c.setFillColor(INK)","+ c.drawRightString(PAGE_W - M, y, mtx)","+ y -= 12","+ if cout.get(\"total_estime\") is not None:","+ c.setStrokeColor(INK)","+ c.setLineWidth(1.2)","+ c.line(M, y + 8, PAGE_W - M, y + 8)","+ c.setFont(\"Helvetica-Bold\", 10)","+ c.setFillColor(INK)","+ c.drawString(M, y - 4, \"Total estimé\")","+ tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"","+ if cout.get(\"annuel_estime\") is not None:","+ tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"","+ c.drawRightString(PAGE_W - M, y - 4, tot)","+ y -= 18","+ if cout.get(\"postes_inconnus\"):","+ c.setFont(\"Helvetica\", 7.5)","+ c.setFillColor(HexColor(\"#8a5a12\"))","+ c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"","+ \"publiées : \"","+ + \", \".join(cout[\"postes_inconnus\"]).lower()","+ + \" — le total réel peut être plus élevé.\")[:125])","+ y -= 12","+ pi2_d = cout.get(\"pi2\")","+ if pi2_d and pi2_d.get(\"valeur\") is not None:","+ v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")","+ suite = f\"{v}{NBSP}$/pi²\"","+ if pi2_d.get(\"percentile_secteur\") is not None:","+ suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"","+ f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"","+ \"secteur (~2 km)\")","+ elif pi2_d.get(\"percentile_ville\") is not None:","+ suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"","+ f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"","+ f\"({pi2_d.get('portee_ville', '')})\")","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ c.drawString(M, y - 2, suite[:120])","+ y -= 12","+ y -= 8","+","+ # --- Historique Lou-Ka (observations réelles des synchronisations)","+ if histo:","+ tl = (histo.get(\"timeline\") or [])[:8]","+ matches = ((recyc or {}).get(\"matches\") or [])[:2]","+ y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)","+ y = s.titre_section(y, \"Historique Lou-Ka\")","+ entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"","+ f\" · {histo.get('jours_en_ligne', 0)} jour\"","+ f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"","+ f\" · {histo.get('modifications', 0)} modification\"","+ f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"","+ f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")","+ pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")","+ if pi_ is not None and pa_ is not None and pi_ != pa_:","+ var_ = histo.get(\"variation\")","+ entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"","+ if var_ is not None:","+ entete_h += (f\" ({'+' if var_ > 0 else '−'}\"","+ f\"{abs(round(var_ * 100))}{NBSP}%)\")","+ c.setFont(\"Helvetica-Bold\", 8.5)","+ c.setFillColor(INK)","+ c.drawString(M, y - 2, entete_h[:120])","+ y -= 16","+ ev_labels = {\"description\": \"Description modifiée\",","+ \"superficie\": \"Superficie modifiée\",","+ \"dispo\": \"Disponibilité modifiée\",","+ \"inclusions\": \"Inclusions modifiées\",","+ \"photos\": \"Photos modifiées\",","+ \"disparition\": \"Annonce retirée\",","+ \"reapparition\": \"Annonce republiée\"}","+ if tl:","+ for it in tl:","+ c.setFont(\"Courier-Bold\", 7)","+ c.setFillColor(INK3)","+ c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ if it.get(\"type\") == \"prix\":","+ if it.get(\"prix_avant\") is not None:","+ sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)","+ else \"monté\")","+ txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"","+ f\"à {_fmt_money(it.get('prix'))}\")","+ else:","+ txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"","+ else:","+ txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")","+ c.drawString(M + 78, y, txt_ev[:95])","+ y -= 11","+ else:","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ c.drawString(M, y, \"Aucune modification observée depuis la première \"","+ \"synchronisation de cette annonce.\")","+ y -= 11","+ for m_ in matches:","+ suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"","+ if m_.get(\"prix\") is not None:","+ suite += f\" — affiché {_fmt_money(m_['prix'])}\"","+ if m_.get(\"derniere_observation\"):","+ suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"","+ c.setFont(\"Helvetica-Bold\", 8)","+ c.setFillColor(HexColor(\"#8a5a12\"))","+ c.drawString(M, y - 2, suite[:115])","+ y -= 10","+ c.setFont(\"Helvetica\", 7)","+ c.setFillColor(INK3)","+ c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])","+ y -= 12","+ y -= 8","+","+ # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)","+ if imm and (imm.get(\"annonces_total\") or 0) >= 2:","+ y = place(y, 128)","+ y = s.titre_section(y, \"Passeport de l'immeuble\")","+ tw = (PAGE_W - 2 * M - 30) / 4","+ rot = imm.get(\"rotation\") or {}","+ pres = imm.get(\"pression_loyers\")","+ tuiles_im = [","+ (str(imm[\"annonces_total\"]),","+ f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),","+ (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),","+ (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),","+ (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"","+ if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),","+ ]","+ for i, (val, lab) in enumerate(tuiles_im):","+ s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)","+ y -= 58","+ par_cc = imm.get(\"loyer_median_par_cc\") or {}","+ if par_cc:","+ parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"","+ for cc, v in sorted(par_cc.items())]","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"","+ + \" · \".join(parts))[:118])","+ y -= 13","+ c.setFont(\"Helvetica-Bold\", 8)","+ c.setFillColor(INK)","+ c.drawString(M, y, \"Rotation des logements :\")","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ if rot.get(\"statut\") == \"calculated\":","+ c.drawString(M + 118, y,","+ (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"","+ f\"annonces sur 12 mois pour ≥{NBSP}\"","+ f\"{rot.get('unites_estimees')} unités\")[:100])","+ else:","+ c.drawString(M + 118, y, \"données insuffisantes\")","+ y -= 12","+ c.setFont(\"Helvetica-Bold\", 8)","+ c.setFillColor(INK)","+ c.drawString(M, y, \"Pression sur les loyers :\")","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ if pres:","+ v12 = pres.get(\"variation_12m\") or 0","+ c.drawString(M + 118, y,","+ (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"","+ f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"","+ f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"","+ f\"{_fmt_money(pres.get('mediane_12m'))} \"","+ f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])","+ else:","+ c.drawString(M + 118, y, \"échantillon insuffisant\")","+ y -= 12","+ c.setFont(\"Helvetica\", 6.5)","+ c.setFillColor(INK3)","+ c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"","+ f\"{_fmt_ts(imm.get('premiere_observation'))} — \"","+ \"indicateurs calculés uniquement sur les annonces \"","+ \"observées (pas un recensement du bâtiment).\")[:135])","+ y -= 16","+","+ # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)","+ g_ = (gest or {}).get(\"google_maps\")","+ if gest and g_ and g_.get(\"statut\") != \"non_associe\":","+ avis_g = gest.get(\"avis\") or {}","+ dist_g = avis_g.get(\"distribution\") or {}","+ tot_g = sum(dist_g.values()) if dist_g else 0","+ y = place(y, 108 + (5 * 11 if tot_g else 0))","+ y = s.titre_section(y, \"Qui gère ce logement?\")","+ c.setFont(\"Helvetica-Bold\", 10.5)","+ c.setFillColor(INK)","+ c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])","+ n_act = gest.get(\"annonces_actives\") or 0","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],","+ \"Helvetica-Bold\", 10.5) + 10, y - 2,","+ f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"","+ f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")","+ y -= 16","+ if g_.get(\"note\") is not None:","+ note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")","+ s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ c.drawString(M + 110, y - 5,","+ (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"","+ f\"« {g_.get('nom', '')} »\")[:90])","+ y -= 22","+ if tot_g:","+ bw_g = 150.0","+ for n_ in (5, 4, 3, 2, 1):","+ cnt = dist_g.get(str(n_), 0)","+ c.setFont(\"Courier-Bold\", 7)","+ c.setFillColor(INK2)","+ c.drawString(M, y, f\"{n_}*\")","+ c.setFillColor(SURFACE)","+ c.setStrokeColor(INK)","+ c.setLineWidth(0.8)","+ c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)","+ if cnt:","+ c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))","+ c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)","+ c.setFont(\"Courier-Bold\", 7)","+ c.setFillColor(INK)","+ c.drawString(M + 22 + bw_g + 6, y, str(cnt))","+ y -= 11","+ y -= 2","+ stats_g = []","+ if avis_g.get(\"moyenne_12m\") is not None:","+ stats_g.append(f\"moyenne des 12 derniers mois : \"","+ f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")","+ + f\" ({avis_g.get('n_12m')} avis)\")","+ if avis_g.get(\"tendance\"):","+ stats_g.append(avis_g[\"tendance\"])","+ if avis_g.get(\"plaintes_frequentes\"):","+ stats_g.append(\"plaintes récurrentes : \"","+ + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))","+ if stats_g:","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ c.drawString(M, y, (\" · \".join(stats_g))[:120])","+ y -= 12","+ c.setFont(\"Helvetica\", 6.5)","+ c.setFillColor(INK3)","+ conf_g = round((g_.get(\"confiance_association\") or 0) * 100)","+ c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"","+ f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"","+ f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"","+ \"Lou-Ka — échantillon des plus récents, pas une \"","+ \"lecture humaine.\")[:135])","+ y -= 16","+"," # --- Registre des loyers (déclarations citoyennes à proximité)"," if rdl_d and rdl_d.get(\"n\"):"," comp = rdl_d.get(\"items\") or []"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c0fd7d5f-ed3a-4432-a169-54126ff26570","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMqh2SJMoP7MC35Mpt3","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISuQMKpwEIERgCKkA2PRVZpmUW+3nzdlBetmUVJq4ZhRBzT0fWT08KtLXHc6DOa1S/q3RTFdsWj4fJMP8yrkcfDq6wP4+OS978m5PoMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGnirPUBrABAhIMyudiwm3C2n99WC7TGgwP+qBfI5KPsMTvr3IiMEgKnuBogEU8cNoeqqm/VRTLW3BIZhI6X1ESnUhQ7Mb6/iHKHQXXINf8XF0CRBDPUiq+AR0Vy80E/bgvtYc/l2gA/ePA+G/nuNYpV8OX6v7agAnN2CyxfSDoKi1y0oqC8tK87hCtQhUIyq1zngeZZJ7r/eENBO6f8Kd8dU4ANq+tH35RqHF8QVnXcYCsg0WVzqotjnpP1aWudaR2Nry8ZnKVqaUbfovi6fBRzKPtjr421fX8CMCRm9OQcjsEZLXvyi4Kgrcnz4FAPQRzXR9VkP2o8/Psh+DOXqUJ1pzaNd45OUwD1i8ND1mGhQaatDFgWX8YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6175,"cache_read_input_tokens":118114,"cache_creation":{"ephemeral_5m_input_tokens":6175,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bcb23764-968c-4a3d-8a21-0131235af784"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMqh2SJMoP7MC35Mpt3","type":"message","role":"assistant","content":[{"type":"text","text":"Maintenant la section « Vie quotidienne en hiver » après KA Scores."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6175,"cache_read_input_tokens":118114,"cache_creation":{"ephemeral_5m_input_tokens":6175,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"200c08c3-960f-4d7d-9784-24e2d80bbbeb"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMqh2SJMoP7MC35Mpt3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014hTwwcm1bQDf5QcnYjHR7G","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier","new_string":" s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":6175,"cache_read_input_tokens":118114,"cache_creation":{"ephemeral_5m_input_tokens":6175,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ac22060f-0a22-4bca-b4cf-62a9a032e8a8"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014hTwwcm1bQDf5QcnYjHR7G","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2f2799e9-8627-471c-a70b-74c66c42aebc","timestamp":"2026-08-24T22:26:55.959Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier","newString":" s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":1109,"oldLines":6,"newStart":1109,"newLines":44,"lines":[" f\"{lab} · {sub}\" if sub else lab)"," y -= 62"," ","+ # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)","+ if hiv:","+ det_h = hiv.get(\"detail\") or []","+ y = place(y, 52 + len(det_h) * 11 + 14)","+ y = s.titre_section(y, \"Vie quotidienne en hiver\")","+ c.setFont(\"Helvetica-Bold\", 17)","+ c.setFillColor(INK)","+ sc_t = str(hiv.get(\"score\", \"—\"))","+ c.drawString(M, y - 8, sc_t)","+ s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,","+ (hiv.get(\"classe\") or \"\").upper())","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK3)","+ c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,","+ \"quotidien à pied, même à −20 °C\")","+ y -= 26","+ for cr_ in det_h:","+ c.setFont(\"Helvetica-Bold\", 8)","+ c.setFillColor(INK)","+ c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])","+ c.setFont(\"Helvetica\", 8)","+ c.setFillColor(INK2)","+ if cr_.get(\"distance_m\") is not None:","+ val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"","+ if cr_.get(\"nom\"):","+ val_h += f\" ({cr_['nom']})\"","+ else:","+ val_h = cr_.get(\"note\") or \"—\"","+ c.drawString(M + 90, y, val_h[:80])","+ c.setFont(\"Courier-Bold\", 7.5)","+ c.setFillColor(INK)","+ c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))","+ y -= 11","+ c.setFont(\"Helvetica\", 6.5)","+ c.setFillColor(INK3)","+ c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])","+ y -= 16","+"," # --- Le quartier"," if quartier:"," y = place(y, 270)"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6a4cb01a-9171-4254-bff5-425d78c910ee","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMreVAyBTwDwGH5THm5","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS3QIKpwEIERgCKkD8/sprMYqza6MATYlf4H9G6iUxXp2AHW/WI20Ggdiu6DF+UvbGMWOinZm4Q+lx4zd5vAGgDejZSEYCw8yGDSM6Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAG0irPUBrABAhIMjnUCCvDlCG1lzm1ZGgwkF1zxZMBwW/DcSMIiML9iRK1ZBnHrBmeNMBmb1aF+LN1alnoMB9JwnIMENtlUullxRA72Gi6zwVhQIUeToSpjynQFiK64qm+6GNQBll3ldlip+J+sXCiPjzIsiB3abtbEuznPRyP9wDC14uKCo7yPSN33E+CBttciDKuAlNPvyDlzZ6L20f3+W0bmWzgy2qdV7DSmfOrCZ9QggS515RHlsFllGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1185,"cache_read_input_tokens":124289,"cache_creation":{"ephemeral_5m_input_tokens":1185,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f20554ce-ed8c-4f6f-88d0-e185be78d8e6"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMreVAyBTwDwGH5THm5","type":"message","role":"assistant","content":[{"type":"text","text":"Je mets à jour l'en-tête du module et je teste la génération PDF."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1185,"cache_read_input_tokens":124289,"cache_creation":{"ephemeral_5m_input_tokens":1185,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"275cac7d-91b4-4e05-86b7-f7e7d5a3d5bc"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMreVAyBTwDwGH5THm5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01P8NLsR2teuY4fnvng7gmCS","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":"# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR","new_string":"# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, coût réel mensuel,\n# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,\n# gestionnaire (fiche Google + avis synchronisés), registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, score hiver, quartier,\n# à proximité, QR"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1185,"cache_read_input_tokens":124289,"cache_creation":{"ephemeral_5m_input_tokens":1185,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"95b8a6c9-2525-457e-b4f5-4ccdfb0c7e3b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01P8NLsR2teuY4fnvng7gmCS","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"061b0e1d-da62-4d0b-bebd-49b516377cf8","timestamp":"2026-08-24T22:27:05.374Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":"# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR","newString":"# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, coût réel mensuel,\n# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,\n# gestionnaire (fiche Google + avis synchronisés), registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, score hiver, quartier,\n# à proximité, QR","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, quartier, à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":4,"oldLines":9,"newStart":4,"newLines":12,"lines":[" # pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka"," # - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la"," # fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),","-# chips, description structurée, inclusions, registre des loyers,","+# chips, description structurée, inclusions, coût réel mensuel,","+# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,","+# gestionnaire (fiche Google + avis synchronisés), registre des loyers,"," # historique TAL, inondation, qualité de l'air, coût d'électricité,","-# commerces/transport, essence, KA Scores, quartier, à proximité, QR","+# commerces/transport, essence, KA Scores, score hiver, quartier,","+# à proximité, QR"," # - rapport_pdf() : rapport global du marché (totaux, régions, histogramme"," # des loyers, types, villes, gestionnaires)"," # Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMreVAyBTwDwGH5THm5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Wo79SwtkCwurFu6GVYubVE","name":"Bash","input":{"command":".venv/bin/python -c \"\nfrom louka import pdfgen\nb = pdfgen.fiche_pdf('app_urbains:archipel-217')\nprint('PDF:', len(b), 'octets' if b else 'ÉCHEC')\nopen('/tmp/fiche-test.pdf','wb').write(b)\n\" 2>&1 | grep -v cede959","description":"Test PDF generation with new sections","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1185,"cache_read_input_tokens":124289,"cache_creation":{"ephemeral_5m_input_tokens":1185,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4f85fd99-0e16-45c0-8fb0-2e7c43993ba7"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Wo79SwtkCwurFu6GVYubVE","type":"tool_result","content":"PDF: 384554 octets","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9d78c902-bb04-403c-b3ab-fc76b0bf7fa2","timestamp":"2026-08-24T22:27:08.024Z","tool_use_result":{"stdout":"PDF: 384554 octets","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"bdaeb5ae-a0d9-41fe-a25f-df7b1b46dd62","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMsY7DWUhLDMzokAuYZ","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISkgcKpwEIERgCKkDvlTnoRyKIXcrqHIZICqbqarGNJmYbCVi/aJV7a/nEUYo20wE98+Z/QsLchVBcCmKoTf2yUL702yVLqmTuzppuMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHDirPUBrABAhIMxlrdG6cS+ArCzG95Ggzgdea8mPSoDvRMXN8iMHuSrcTDuObh4O2eQObicY8Bt8a1gXBn/KdYQg8yTfJwKibV6SOrI9oWBZEMFZJnfSqXBSniUMxCUXnVCbEK9S5csiG7Tzxp6lIT/HDEpQzls0kbbzsx5xw13zXG/C0eL4sRazjI1ANFfRGAwWhZdM9OH6808TRT/jSwhxyAcJHvupMzMWXrRlOul+C2AUEg1ApGAEvyp0B1k9NdIMT5YOh9giYB2LtK2ioIZryp86Xis22c/FB+7V+Y9eBObzZTmDNOh/+oHj/QZsETXBTITzz28rbHA0Mn6jReB0eK5UW8w5fqqKdxwbqE1z5poIxV9keZvyPVseCgyTxytDJiDgu+MLY3ia5cx73riF2ekAfClBrnrezTP24tlrInb+OSQcQsowFabIkW3O45ci7U5xKPeoyRM2FfjeWIJUSWGZbqGZFd/8U+fOAZbwYVN0JmvKhjFOLTyXCGBIbvoMJhIoaeJcLjiypTSamV8us5Ivh4BaesWwhB14bn7HAuSmx4mM4oOrDAUU0svZYJNPKKP80ZS3xzBhFJacPAsxX7N/QPp09Ho4608knqLQSyhe873lVT2fjx4RrSN770/8aolNWb/4pdpr1mDvvbMpetOaYrfdSma0QFKWzsxSPnrInXHN1F0WyoM0DxEj2UzNCHX+GHYn/Mh/nTu0Vj7cp4gzLUjVhldVyM5V8XCqdPvB0q9wDhFPFakF6De93qJqefZq7inubdPlaDW00VImKswUwZHQNBRtM5WkUPDVAWxtiiCixIpdQEKK/zWOAZnCLSyRBaZkn/hbDToMyW2aSj+9DxeLa22PggzA02iI9uCTxY9OI9+C0c1VxFbiCBeNTW7ivozA9IC1L+3ZrQ9yzdsX05K9q5tjTJgtn4xesyKqLHYwrfo3ISiZdaYyeX6sX4xJHIjFOEWTrCXmesEy8OP13I8WRsVCfgRYa46BgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1226,"cache_read_input_tokens":125474,"cache_creation":{"ephemeral_5m_input_tokens":1226,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"18287c9a-ae3a-4a78-80dd-620abb26968f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMsY7DWUhLDMzokAuYZ","type":"message","role":"assistant","content":[{"type":"text","text":"PDF généré (384 Ko). Je vérifie visuellement les nouvelles sections en le convertissant en images."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1226,"cache_read_input_tokens":125474,"cache_creation":{"ephemeral_5m_input_tokens":1226,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d9712465-265e-4828-8588-6b756828e72a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMsY7DWUhLDMzokAuYZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RCLDFQemgYwtx1qn6Am59E","name":"Bash","input":{"command":"which pdftoppm pdftotext 2>/dev/null; .venv/bin/python -c \"import pypdf\" 2>&1 | head -1","description":"Check PDF tooling availability"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1226,"cache_read_input_tokens":125474,"cache_creation":{"ephemeral_5m_input_tokens":1226,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"27f874f0-e5b8-42d6-a1a1-6d0ad733f0d4"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RCLDFQemgYwtx1qn6Am59E","type":"tool_result","content":"/opt/homebrew/bin/pdftoppm\n/opt/homebrew/bin/pdftotext\nTraceback (most recent call last):","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"aa64026b-74ae-4a37-8899-7372bb38755f","timestamp":"2026-08-24T22:27:16.173Z","tool_use_result":{"stdout":"/opt/homebrew/bin/pdftoppm\n/opt/homebrew/bin/pdftotext\nTraceback (most recent call last):","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"fcaabf4e-ad86-4f7e-bc3b-fec6a45f1860","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMt98skPD2eeEGfQS3i","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011pUM9ozzyttFteFhqRvSdR","name":"Bash","input":{"command":"pdftotext /tmp/fiche-test.pdf /tmp/fiche-test.txt && grep -n \"Coût réel\\|Historique Lou-Ka\\|Passeport de l'immeuble\\|Qui gère\\|Vie quotidienne en hiver\\|Total estimé\\|Note Google\" /tmp/fiche-test.txt; echo \"---pages---\"; pdftoppm -png -r 70 /tmp/fiche-test.pdf /tmp/fichepg && ls /tmp/fichepg*","description":"Extract text and render PDF pages to PNG"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":473,"cache_read_input_tokens":126700,"cache_creation":{"ephemeral_5m_input_tokens":473,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a53d6b8d-a993-43dc-b11a-10e16728effa"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011pUM9ozzyttFteFhqRvSdR","type":"tool_result","content":"60:Coût réel mensuel\n81:Total estimé\n87:Historique Lou-Ka\n91:Passeport de l'immeuble\n310:Vie quotidienne en hiver\n---pages---\n/tmp/fichepg-1.png\n/tmp/fichepg-2.png\n/tmp/fichepg-3.png\n/tmp/fichepg-4.png","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"104f553a-2ae6-44fe-bc16-7643f2a529fa","timestamp":"2026-08-24T22:27:24.580Z","tool_use_result":{"stdout":"60:Coût réel mensuel\n81:Total estimé\n87:Historique Lou-Ka\n91:Passeport de l'immeuble\n310:Vie quotidienne en hiver\n---pages---\n/tmp/fichepg-1.png\n/tmp/fichepg-2.png\n/tmp/fichepg-3.png\n/tmp/fichepg-4.png","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"db10c3e7-8966-44a8-abfa-90292e51d910","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMtkb5fbVb8RsamwTpe","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISgwQKpwEIERgCKkBpyw7/Usj7/BNWVZOvbvFNtWOBsI+MUHT0kkVWl5ZOEx8mATObd3+tRdDAO4aVwPL7TJOpmYFIKhWp5zTmIjSDMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHRirPUBrABAhIMX8xB1bNCEm3rgYCtGgyuTEJ5ao2UWzgcrZoiMBApnAi0iyxeW4lGjspUrJKspm4cwyftxrv+H+cq04j6ZKRvK7kFzNf4V4OaNg/wmyqIAqKSAUtQF66cEhInY3N1l2mIQYNljfoO8JRBv/fKpN7V/KSaRdNWnlyWh844rqhn5jV2TXGcnlRdLgOgddCa9v5goIk2ssgyrIvPA9cMTSADAV8VT4FNjeTi8ykKrt+uIxZoKaSkccoT7tLJ0WACUIvCL36vkJTLJWI8liIBzc+JHnoHbyS5i21/CdUjSTOHANrp29s1g2g616NgSrY/qF8m9kxGGWSsXWDNXooM6N5Ns0IQRHfb6/pTiktOp6mAB9kuHLBABo6V7pwQuganJvxmCFEexCM3vtcUO39dFr8WCPFbamKwMu8RhrLr7OfH2z2hX75hktKXDb244SPYW7aPNb8vU3bgbhgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":383,"cache_read_input_tokens":127173,"cache_creation":{"ephemeral_5m_input_tokens":383,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7fd5b24c-e50c-4fd3-9f8f-470f88b34ce0"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMtkb5fbVb8RsamwTpe","type":"message","role":"assistant","content":[{"type":"text","text":"Les sections apparaissent (gestionnaire absent car app_urbains n'a pas de fiche Google — attendu). J'inspecte le rendu visuel."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":383,"cache_read_input_tokens":127173,"cache_creation":{"ephemeral_5m_input_tokens":383,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"c1091a81-705b-40bf-b042-a4cf35c425a6"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMtkb5fbVb8RsamwTpe","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014NWCUXe54KmQDFCaJCHFsC","name":"Read","input":{"file_path":"/tmp/fichepg-2.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":383,"cache_read_input_tokens":127173,"cache_creation":{"ephemeral_5m_input_tokens":383,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"051c8001-d20e-4096-8bd9-ba16365316f2"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014NWCUXe54KmQDFCaJCHFsC","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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X3TBORf/57UVuw6GIvc9+NitN37jTZZUluWbf3BvS3N938D4Zy84+9HH/qZ007ZYzA/85sljj143MDR550+vVzfd3tM7onT6djpsf37q+dpqt81mvejCs2+85a4NR67xzQWV7ie/fvjJiy44W9nUj2+75qZb76bL/Pd1l/+/H/36tu9f9bd/bK6pds/MzvcNjAcC4RxTyGSZD5509HN/e7WhvnZsfPb6a7/47N82sSXOPx8695MfHh2bDSwseus8LMsdvrYjEAin07lHHn1Waer9yKPPKiu++T9RBwD4l33O97M7H77m+h9+64afnHzixuGRyY725uu/+aUrvnjhU3954fTTTjh6w2GNDXWfOucjR21YW1vjGR2bPv20E87+2GnK6ru6B2+75Zpf3vG9h377JCGkvbXplu9dddEF54yO7W0BPDg0saqr9cYbvnbJxecSQp58+oX6+tr2tmYmX+wfGMtkmKoqx7VXf7FiVE88/b9fu+Li6665vKOjeXBo4v4HH/v1Pbf+5PbrzBYTIeTh3z117DFHnnLycfc/9GdCyJcuu/CG665oavTe/N0r6Z9F3v2rR0884ej1R659+pkXCSEfPfOU7377K40NdUy+QMfP88LSXTc31b/52COETM8EWpq8l1/67+d87FRCSEU37RM+sOGyL3y6tsZdLLLqptvq7tWPP/XC+iNWr+pqfXVLtygITL5ot1s//9lPVuxI2RTDFJZdxmo1r13T/qFTjjvumCNOOWkj7cvK5IuSJH3uM5+wWc2nffC41ata3S4n/Z6588898ytf+kw0ljxm47rVq9tYtqxu6q1eEVcsALyLk++6ay7/xc++++wT9/7luZcEQaRNh/OFktm8/9HtW//947ra6rM+8sFlX98QQkosS7szW20WQojBoJf2daAWRJE2jqL3CILQ2FC7Yf3aSy4+t76h9q6ff6+ttfHK/7qlotelMpJCoWS2mDUajV6vE0WxVGTpe5Ib1q89duMRl37+04QQu82q1WpNJoMyHkEQV69q27B+7TeuvIQQQnuD6fX7RyXJ0kq7fvOUfs20OfKSbtomQohOr5Nk+TVNt2VpVVfrYWs6zjn7NFGSWprrV3W1fuaCj0uyfN3Vl9Z7a37yPw/T7xdUKJsSRUlZpsxytAi5Je85U5+/6Jzjjjni8adfHBmb+cU9j7rdzuOPW09/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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"fd5242ad-ddfa-4a2b-9b40-215eb959530d","timestamp":"2026-08-24T22:27:30.139Z","tool_use_result":{"type":"image","file":{"base64":"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X3TBORf/57UVuw6GIvc9+NitN37jTZZUluWbf3BvS3N938D4Zy84+9HH/qZ007ZYzA/85sljj143MDR550+vVzfd3tM7onT6djpsf37q+dpqt81mvejCs2+85a4NR67xzQWV7ie/fvjJiy44W9nUj2+75qZb76bL/Pd1l/+/H/36tu9f9bd/bK6pds/MzvcNjAcC4RxTyGSZD5509HN/e7WhvnZsfPb6a7/47N82sSXOPx8695MfHh2bDSwseus8LMsdvrYjEAin07lHHn1Waer9yKPPKiu++T9RBwD4l33O97M7H77m+h9+64afnHzixuGRyY725uu/+aUrvnjhU3954fTTTjh6w2GNDXWfOucjR21YW1vjGR2bPv20E87+2GnK6ru6B2+75Zpf3vG9h377JCGkvbXplu9dddEF54yO7W0BPDg0saqr9cYbvnbJxecSQp58+oX6+tr2tmYmX+wfGMtkmKoqx7VXf7FiVE88/b9fu+Li6665vKOjeXBo4v4HH/v1Pbf+5PbrzBYTIeTh3z117DFHnnLycfc/9GdCyJcuu/CG665oavTe/N0r6Z9F3v2rR0884ej1R659+pkXCSEfPfOU7377K40NdUy+QMfP88LSXTc31b/52COETM8EWpq8l1/67+d87FRCSEU37RM+sOGyL3y6tsZdLLLqptvq7tWPP/XC+iNWr+pqfXVLtygITL5ot1s//9lPVuxI2RTDFJZdxmo1r13T/qFTjjvumCNOOWkj7cvK5IuSJH3uM5+wWc2nffC41ata3S4n/Z6588898ytf+kw0ljxm47rVq9tYtqxu6q1eEVcsALyLk++6ay7/xc++++wT9/7luZcEQaRNh/OFktm8/9HtW//947ra6rM+8sFlX98QQkosS7szW20WQojBoJf2daAWRJE2jqL3CILQ2FC7Yf3aSy4+t76h9q6ff6+ttfHK/7qlotelMpJCoWS2mDUajV6vE0WxVGTpe5Ib1q89duMRl37+04QQu82q1WpNJoMyHkEQV69q27B+7TeuvIQQQnuD6fX7RyXJ0kq7fvOUfs20OfKSbtomQohOr5Nk+TVNt2VpVVfrYWs6zjn7NFGSWprrV3W1fuaCj0uyfN3Vl9Z7a37yPw/T7xdUKJsSRUlZpsxytAi5Je85U5+/6Jzjjjni8adfHBmb+cU9j7rdzuOPW09/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","type":"image/png","originalSize":67402,"dimensions":{"originalWidth":595,"originalHeight":770,"displayWidth":595,"displayHeight":770}}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"4d7086a2-4ae6-4a18-bce1-d6514cfe6aa1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMuBFtxiG2vio64gKmj","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIShAcKpwEIERgCKkD9XBdTbnd389tpN6TonxR3yAI5fHyZA8cMxg1fGNUsSMHWpaw6nVmQPcOiz2ML8IeBKIDHvZFMrdPM5wvUr5ceMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHcirPUBrABAhIMGVZc6N7ZUMm+yGiZGgxAmIZ8SFkwMqEJTaMiMFNaKcMvObxsq0YwdF0F8U5rHIe8rg4C/kOu9GIQhMmtso5BwRhJnPmMIC6QhIJFDSqJBeMfU+CkGBfYQ+juT5x99BiTrH6Md2ZUzJUH7cAx02dd8TsjnEVGeRRN4BGcmN3MlrjBUTjhEq0CuEZ7Afbop+d/wz+Q+dg3wgfKtm6MRG7RqgfIaM9lWlzg5Ft0zC/m4kl/x9Rg57qJbsRnR9xsjI5G5C2krQsN82bzg/UtTbBsPiow8R+aDWs2f3H1Abd1rUd+MFpcKeZMU+imN9OU+5FFI37/CM4xnW9NTzVudUECRN6OIT4pl21b4i/7aP6i0KIreNs3K+J9FFktPhOYqqgVsJZKNLyXduGCTN1kbHnpg+w6nYc1w6rQydR1B/JX+3oOUmOKSvsJrJwAYSMjSxKuRDPDe9uak9yYGGipUEzMiNjLhwowJzejaAqKyLsNQ88Aq2hqD7J67EJTeQ/2EJczOHtcDRqExwI6kI41NgCZRsRMTWwdZB1AAZfHjutblX/ewaZNmWI4qFNkc5t8pxnL3v6n0RniMHBsRBD/aa2TZmy2kPITNHQEqBEDkkJsCx3VF1ESCS22T0q7JqF4wXcNwioezaqEjDxJewhbddGqIe8ty9aOa7iEl/MbyXQzZezS8+8zTOG9IbI+PcE71Bzoy1ZXK8Zmqv3QAVlqYqO72EEZI/y4k9GNq821ri6gu3Fd/5aq9LrwUxoYCK+qUcwHn37uTDQIZGVpU7bh/GY8TSDf4TKHYu7Z+bmIxbhijBAqTqrLR41RHUgTZBszc5RPx5bZu1Ip8d7k6YS5HjqU1JXXUg7rpyEeqWFkO/QF9XYzljWwGdUzv+cnPulX9QwslONunW99YtxzNydTwNVomvZYnDzYWh2gAJd0Q/Qm33HOf/a9gE/RtP5VdVHNwjChOUplqQYzNFMYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":838,"cache_read_input_tokens":127556,"cache_creation":{"ephemeral_5m_input_tokens":838,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2783d6da-2040-4e68-bf40-bdf06a0c337f"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMuBFtxiG2vio64gKmj","type":"message","role":"assistant","content":[{"type":"text","text":"Page 2 impeccable — tableau du coût réel avec étiquettes, tuiles du passeport. Je vérifie la section hiver et je teste le gestionnaire sur une annonce Logisco."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":838,"cache_read_input_tokens":127556,"cache_creation":{"ephemeral_5m_input_tokens":838,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"88a39705-b559-45b5-b61b-513b81ddd75b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMuBFtxiG2vio64gKmj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017JuBNV74GDfjWbymn3JEQw","name":"Bash","input":{"command":"grep -n \"Vie quotidienne\" /tmp/fiche-test.txt >/dev/null && pdftotext -f 1 -l 9 /tmp/fiche-test.pdf - | grep -n -A2 \"Vie quotidienne\" | head -8; UID_L=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nr = con.execute(\\\"SELECT uid FROM listings WHERE active=1 AND source='logisco' LIMIT 1\\\").fetchone()\nprint(r[0] if r else '')\") && .venv/bin/python -c \"\nfrom louka import pdfgen\nb = pdfgen.fiche_pdf('$UID_L')\nopen('/tmp/fiche-logisco.pdf','wb').write(b)\nprint('PDF logisco:', len(b))\" 2>&1 | grep -v cede959 && pdftotext /tmp/fiche-logisco.pdf - | grep -n \"Qui gère\\|Note Google\\|plaintes\\|confiance\" | head","description":"Check hiver section and gestionnaire on Logisco PDF","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":838,"cache_read_input_tokens":127556,"cache_creation":{"ephemeral_5m_input_tokens":838,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2f434a60-73c2-4d5a-9ae7-e04f4b2a4db0"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017JuBNV74GDfjWbymn3JEQw","type":"tool_result","content":"310:Vie quotidienne en hiver\n311-\n312-41\nPDF logisco: 463804\n56:fourchette 1 765 $ – 2 306 $ · confiance fort · 12 comparables · segment de 51 annonces\n125:Qui gère ce logement?\n130:Note Google 3,6 / 5\n154:moyenne des 12 derniers mois : 1,5 (8 avis) · plaintes récurrentes : service client, communication, entretien, bail et l\n155:Fiche Google Maps associée automatiquement (confiance 100 %). Statistiques sur les 8 avis synchronisés dans la base Lou-Ka — échantillo","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"662f0cfe-3ba4-4cda-bda5-19fe6e16260f","timestamp":"2026-08-24T22:27:45.771Z","tool_use_result":{"stdout":"310:Vie quotidienne en hiver\n311-\n312-41\nPDF logisco: 463804\n56:fourchette 1 765 $ – 2 306 $ · confiance fort · 12 comparables · segment de 51 annonces\n125:Qui gère ce logement?\n130:Note Google 3,6 / 5\n154:moyenne des 12 derniers mois : 1,5 (8 avis) · plaintes récurrentes : service client, communication, entretien, bail et l\n155:Fiche Google Maps associée automatiquement (confiance 100 %). Statistiques sur les 8 avis synchronisés dans la base Lou-Ka — échantillo","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8a5ce7be-1ec0-431a-905e-9261afdb188d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMvKiv2vmWBbsBuyEXo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WFB1RL2e24iVVLVejhZHxS","name":"Bash","input":{"command":"pdftoppm -png -r 70 -f 2 -l 2 /tmp/fiche-logisco.pdf /tmp/logiscopg && ls /tmp/logiscopg*; pdftoppm -png -r 70 -f 4 -l 4 /tmp/fiche-test.pdf /tmp/hiverpg && ls /tmp/hiverpg*","description":"Render gestionnaire and hiver pages to PNG"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":974,"cache_read_input_tokens":128394,"cache_creation":{"ephemeral_5m_input_tokens":974,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"145c8cfc-bfb1-4434-8bff-2a179078c2a6"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WFB1RL2e24iVVLVejhZHxS","type":"tool_result","content":"/tmp/logiscopg-2.png\n/tmp/hiverpg-4.png","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"e22010dd-02e5-42a5-8665-fe448f48c63a","timestamp":"2026-08-24T22:27:54.195Z","tool_use_result":{"stdout":"/tmp/logiscopg-2.png\n/tmp/hiverpg-4.png","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"bcbf891b-5c16-474f-b6d2-0014c2877930","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMvx3iTS7KippJLxRq7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013JnqHCY9irTWiKLBLxkYzj","name":"Read","input":{"file_path":"/tmp/logiscopg-2.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":243,"cache_read_input_tokens":129368,"cache_creation":{"ephemeral_5m_input_tokens":243,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"98707005-2e14-404d-b854-41f3ecdf2b85"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013JnqHCY9irTWiKLBLxkYzj","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"606e783d-ee65-41bc-87db-d49c6ced3a01","timestamp":"2026-08-24T22:27:58.345Z","tool_use_result":{"type":"image","file":{"base64":"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","type":"image/png","originalSize":84187,"dimensions":{"originalWidth":595,"originalHeight":770,"displayWidth":595,"displayHeight":770}}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMvx3iTS7KippJLxRq7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PqkisXQL8nQvUYYMfe3r5h","name":"Read","input":{"file_path":"/tmp/hiverpg-4.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":243,"cache_read_input_tokens":129368,"cache_creation":{"ephemeral_5m_input_tokens":243,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a79bbcf5-515b-440a-a8a5-89a36abf0b48"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PqkisXQL8nQvUYYMfe3r5h","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8d891392-4257-4f18-a325-c4112ec56842","timestamp":"2026-08-24T22:27:58.413Z","tool_use_result":{"type":"image","file":{"base64":"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","type":"image/png","originalSize":85915,"dimensions":{"originalWidth":595,"originalHeight":770,"displayWidth":595,"displayHeight":770}}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"4103a3c8-d528-41b5-bcc9-31207c5a8c22","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMwFkaGrAKhj99KxqEh","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISjQoKpwEIERgCKkCJBEqtqnTC9XvB0k1gZ4G9tx+8tOkxv94nEDoekalm/ngSEn1JewQl8wUKHvEIOEM1mMJOyxPQuornriV0c5ZzMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH6irPUBrABAhIMaVXg2Q1LapZLbGzRGgwiGxGKBlx/WjD6NhoiMOIZ81oA5cakfbN/W04pIDH87V5LkYpAb/umu1QEq41Hp/pLExwus1/oQgMZOplmwCqSCJixr3jnFPzjRje5BGRH9grpiYp0Y3Fy21LO+Pw/8l8PIiN+26dpLK7bpw3Yw5tSUEqRKspMtQUN1n1tOPouecMNRqjedU+yDxNGYWS5X6rQKwGxQvDBcbAFZhzhU7iDTSckJjjfaLS7PTzjnuQQQtPg2TLaF07xBYOFBuUNLan/9tGCwb1ITTkx5uIing1Gwnr/7kqU6ubmUsaIbLxiBM3/sCfxdLoMe5GnB4Ysx6RO+bqUOIdsJx5pHKQcsWb3VS/7SSVY420uj/1AYDYZMsfpQ8IayYKIriPVYZOo0dDsHWm3AU+hLP7KM3qm9oakjph0ufOK0+Muj6KFKIfb8DBnekfNDUEt3+E+q7+3W+hFIrFhr2UH2hGQYMLlpSkJCpznWfo3CqPa+p9LqK2Ngeld75J62H+DLLLCYMti9vmWcdTWiG82rPyQGwXT9+xji1T5vCo6b8iut+4uoBdNsXunKJnXgyJEgwBNWuoH5VDWcFAlJEsSQEwgIrbQt7qi5gLrWsyBvXa+cM0LlGy51Mp4g95XaH1UaEtiMRV+DyeQlbZ8V9kU+4qDzeM356bGU6fCcRvmawI+zV2dRFXRdfp396OUzkcMG3jnDT4Yv3coJ0Rr50cTabhgUGwzmNutGGTFIq9a2OYPfZFJMfTI/cgH5U+FRJqwLmZPyZmKPXOmYwlkU1YkeBjG9EodF0SzQ26kHjbyeVR6Kxt7KfdyVhA6j1Dzva6xUgPpZH6UWLwZf/tZDr25LhPSCF6EIbWE6ZE6uvFe6eEcvlBmILAQRty6DhWHCqV5EoSofelmk1T5zfPD4Z3CFjKCIQ8ESadx5oFGKh/LeWRquIbqLhoSbDL7PIwXt+lT+xkD39tMHaB33jiAXZ+V1YI+rWF90S1VE4hPmCAMv5mSJGFO3+q+POwsIgY1hofj8JZpVjSLuG7QPf+tA+iFV2Hgh+fYZMV14CVXIy6C0qaMSCdc7dLEN906J0IY/p7dQWrx3Vh7kzym0btX58Cafqrf1nGslPJw4dyureXguWuJMXINvVUp5jkRgSBsv0ZjlKwXZ1cbU9IS5LRD9po5YN/St7yesPPkurpGnwoEDCsjPEijK8uoC9BiZTzJKa11q/OrJKPlCSt2cwaDDaxrueoUp6JOlVl7wlze78uRUaxp4z24TPSkdjFaaIVBYh9jmskkqU8r/CaQ+VAwwIya7uW8MOFIR8slDYc2yYC4tUh0fyqN7zYZtEMNmkEkjP9FQmDzjtgljaDXj0cWWFG9YDqKLjPoZq6vxMGSCi/dpxdjI2/VMhn7YCvMMRUW2xBwbdUy7GXzbV5x0Nja9kfaBxe3CzqjrdcciENvRKQiCKPJrlHT9jtwDwQsfdzOi05j/OXGxUAEcR4ks40YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1737,"cache_read_input_tokens":129611,"cache_creation":{"ephemeral_5m_input_tokens":1737,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a065cd55-96a8-4bc0-8a94-f09aaaf7b210"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMwFkaGrAKhj99KxqEh","type":"message","role":"assistant","content":[{"type":"text","text":"Le PDF est bon. Je corrige trois textes coupés au milieu d'un mot (postes inconnus, stats gestionnaire, note fine) en les passant en multi-lignes."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1737,"cache_read_input_tokens":129611,"cache_creation":{"ephemeral_5m_input_tokens":1737,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"cacd46ea-f3e1-4eb6-babb-64a60b1a3bb7"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMwFkaGrAKhj99KxqEh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BcA4E2twL6eHjAwf3qTEDS","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12","new_string":" if cout.get(\"postes_inconnus\"):\n y = s.paragraphe(M, y - 2, \"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\",\n PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1737,"cache_read_input_tokens":129611,"cache_creation":{"ephemeral_5m_input_tokens":1737,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"947a55aa-9953-448a-930b-c867800797c3"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BcA4E2twL6eHjAwf3qTEDS","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"3a042f08-dbca-4744-8d8d-607a36c05eb0","timestamp":"2026-08-24T22:28:14.567Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12","newString":" if cout.get(\"postes_inconnus\"):\n y = s.paragraphe(M, y - 2, \"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\",\n PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, coût réel mensuel,\n# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,\n# gestionnaire (fiche Google + avis synchronisés), registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, score hiver, quartier,\n# à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\")[:125])\n y -= 12\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":633,"oldLines":13,"newStart":633,"newLines":13,"lines":[" c.drawRightString(PAGE_W - M, y - 4, tot)"," y -= 18"," if cout.get(\"postes_inconnus\"):","- c.setFont(\"Helvetica\", 7.5)","- c.setFillColor(HexColor(\"#8a5a12\"))","- c.drawString(M, y - 2, (\"Postes non chiffrables avec les données \"","- \"publiées : \"","- + \", \".join(cout[\"postes_inconnus\"]).lower()","- + \" — le total réel peut être plus élevé.\")[:125])","- y -= 12","+ y = s.paragraphe(M, y - 2, \"Postes non chiffrables avec les données \"","+ \"publiées : \"","+ + \", \".join(cout[\"postes_inconnus\"]).lower()","+ + \" — le total réel peut être plus élevé.\",","+ PAGE_W - 2 * M, taille=7.5,","+ couleur=HexColor(\"#8a5a12\"), interligne=9.5,","+ max_lignes=2)"," pi2_d = cout.get(\"pi2\")"," if pi2_d and pi2_d.get(\"valeur\") is not None:"," v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMwFkaGrAKhj99KxqEh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TrKoiMb7oEcS8ythfYPYXo","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16","new_string":" if stats_g:\n y = s.paragraphe(M, y, \" · \".join(stats_g), PAGE_W - 2 * M,\n taille=8, couleur=INK2, interligne=10,\n max_lignes=2)\n y -= 2\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n y = s.paragraphe(M, y - 2, f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur \"\n f\"les {avis_g.get('n', 0)} avis synchronisés dans \"\n \"la base Lou-Ka — échantillon des plus récents, \"\n \"pas une lecture humaine.\",\n PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=2)\n y -= 6"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1737,"cache_read_input_tokens":129611,"cache_creation":{"ephemeral_5m_input_tokens":1737,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"f8a6bc47-6d1c-4f7a-a3a3-3b123416b9ae"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TrKoiMb7oEcS8ythfYPYXo","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"89e47b24-4b3e-4e96-bf36-db6d9bec3016","timestamp":"2026-08-24T22:28:19.984Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16","newString":" if stats_g:\n y = s.paragraphe(M, y, \" · \".join(stats_g), PAGE_W - 2 * M,\n taille=8, couleur=INK2, interligne=10,\n max_lignes=2)\n y -= 2\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n y = s.paragraphe(M, y - 2, f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur \"\n f\"les {avis_g.get('n', 0)} avis synchronisés dans \"\n \"la base Lou-Ka — échantillon des plus récents, \"\n \"pas une lecture humaine.\",\n PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=2)\n y -= 6","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, coût réel mensuel,\n# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,\n# gestionnaire (fiche Google + avis synchronisés), registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, score hiver, quartier,\n# à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n y = s.paragraphe(M, y - 2, \"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\",\n PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\" · \".join(stats_g))[:120])\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"\n f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"\n \"Lou-Ka — échantillon des plus récents, pas une \"\n \"lecture humaine.\")[:135])\n y -= 16\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":851,"oldLines":19,"newStart":851,"newLines":19,"lines":[" stats_g.append(\"plaintes récurrentes : \""," + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))"," if stats_g:","- c.setFont(\"Helvetica\", 8)","- c.setFillColor(INK2)","- c.drawString(M, y, (\" · \".join(stats_g))[:120])","- y -= 12","- c.setFont(\"Helvetica\", 6.5)","- c.setFillColor(INK3)","+ y = s.paragraphe(M, y, \" · \".join(stats_g), PAGE_W - 2 * M,","+ taille=8, couleur=INK2, interligne=10,","+ max_lignes=2)","+ y -= 2"," conf_g = round((g_.get(\"confiance_association\") or 0) * 100)","- c.drawString(M, y - 2, (f\"Fiche Google Maps associée automatiquement \"","- f\"(confiance {conf_g}{NBSP}%). Statistiques sur les \"","- f\"{avis_g.get('n', 0)} avis synchronisés dans la base \"","- \"Lou-Ka — échantillon des plus récents, pas une \"","- \"lecture humaine.\")[:135])","- y -= 16","+ y = s.paragraphe(M, y - 2, f\"Fiche Google Maps associée automatiquement \"","+ f\"(confiance {conf_g}{NBSP}%). Statistiques sur \"","+ f\"les {avis_g.get('n', 0)} avis synchronisés dans \"","+ \"la base Lou-Ka — échantillon des plus récents, \"","+ \"pas une lecture humaine.\",","+ PAGE_W - 2 * M, taille=6.5, couleur=INK3,","+ interligne=8.5, max_lignes=2)","+ y -= 6"," "," # --- Registre des loyers (déclarations citoyennes à proximité)"," if rdl_d and rdl_d.get(\"n\"):"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"90b207a5-b22b-42b8-bd44-262d01af4c53","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMxrSB5PpScyJg5wcCS","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01C8jKDkRYthM5EnzXUY8G56","name":"Bash","input":{"command":".venv/bin/python -c \"\nfrom louka import pdfgen\nfor uid, out in (('app_urbains:archipel-217','/tmp/fiche-test.pdf'), ):\n b = pdfgen.fiche_pdf(uid); open(out,'wb').write(b); print(out, len(b))\n\" 2>&1 | grep -v cede959 && UID_L=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nprint(con.execute(\\\"SELECT uid FROM listings WHERE active=1 AND source='logisco' LIMIT 1\\\").fetchone()[0])\") && .venv/bin/python -c \"\nfrom louka import pdfgen\nb = pdfgen.fiche_pdf('$UID_L'); open('/tmp/fiche-logisco.pdf','wb').write(b); print('logisco', len(b))\" 2>&1 | grep -v cede959 && pdftotext /tmp/fiche-logisco.pdf - | sed -n '/Postes non chiffrables/p;/lecture humaine/p'","description":"Regenerate both PDFs and verify full text lines","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1845,"cache_read_input_tokens":131348,"cache_creation":{"ephemeral_5m_input_tokens":1845,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a6c5d4fd-ac4c-48a3-935b-a795c04ba7ab"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01C8jKDkRYthM5EnzXUY8G56","type":"tool_result","content":"/tmp/fiche-test.pdf 384569\nlogisco 463899\nPostes non chiffrables avec les données publiées : électricité, chauffage, eau chaude, stationnement — le total réel peut être plus élevé.","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5b7c25e0-01ed-49a0-814e-1d465c2807ea","timestamp":"2026-08-24T22:28:29.381Z","tool_use_result":{"stdout":"/tmp/fiche-test.pdf 384569\nlogisco 463899\nPostes non chiffrables avec les données publiées : électricité, chauffage, eau chaude, stationnement — le total réel peut être plus élevé.","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c297a648-a76e-408f-a9d7-b5dfeda9e0bc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMyYVTTWXrAEhCXTMZi","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISpwMKpwEIERgCKkBSBjiGwbvmMDRABPjuJiC7aW8T2TOctDI6tXETdiasY9U78fcO51W3HuaA9OPfLtAMArNB90JYavHSp+1fqCbhMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGRi7PUBrABAhIM53yeO74E0F98F87/GgwFyrhUhij7AZs8Pj8iMGRoB9+jVhMi7wJc0zj+wrWPuzH5zaLjTneBFvP4yGz5lfRkXCuJXpfz7vlUXngXkyqsAVpfp4nkJDwbFt7GQwp1HL9kyhJ3ayL7XG4xhNnwDjD/EIS+1J9U9y3fTyS9g1YlepScYpdmZvBbk3ZXqSV6WwfkLI4UsPj/+idsXIiCnXgZv5bQYIamMVVHWwpB7WwGEPtco+IpzMPfqHafcZ5RSVM5gV6Q/IYmxruwqhDrBwiuZ2EVsws5O4t/U55DdrTYu+5tllK+wuusNGA5JebpLZOPk4475Yvotl++gqYYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":543,"cache_read_input_tokens":133193,"cache_creation":{"ephemeral_5m_input_tokens":543,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"4d22d869-54c7-4d5f-9d10-c816e618a581"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMyYVTTWXrAEhCXTMZi","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019nwVnnL2bpAzD6LYQgFBXw","name":"Bash","input":{"command":"pdftotext /tmp/fiche-logisco.pdf - | grep -n -A1 \"Fiche Google Maps associée\" ","description":"Verify gestionnaire fine note complete"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":543,"cache_read_input_tokens":133193,"cache_creation":{"ephemeral_5m_input_tokens":543,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"44ad3f50-f42c-44b7-a19b-69b452417edf"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019nwVnnL2bpAzD6LYQgFBXw","type":"tool_result","content":"155:Fiche Google Maps associée automatiquement (confiance 100 %). Statistiques sur les 8 avis synchronisés dans la base Lou-Ka — échantillon des plus récents, pas une lecture\n156-humaine. …","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b6eb297d-b7f5-42f2-b998-8bc42c28f4b6","timestamp":"2026-08-24T22:28:34.931Z","tool_use_result":{"stdout":"155:Fiche Google Maps associée automatiquement (confiance 100 %). Statistiques sur les 8 avis synchronisés dans la base Lou-Ka — échantillon des plus récents, pas une lecture\n156-humaine. …","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c2c0b5ea-dead-49b1-8c3b-121a48c74d2e","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMyxkxea3guLy9g1sGE","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS8QcKpwEIERgCKkBSDp5oGMKcA6HD3RIY3VJf9RJNe8A8C+SxCp5TdmVjvkvs8A2+Bg4daEd7AxaPuacJWV6eWJ+s0mGVPTp8IM9iMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGei7PUBrABAhIMJiTTrLvmPWES3z8jGgw0hBCNDG2411FBVwwiMN6Sdu+T+jv2W0kUJYC8xQ5UqAh81AC2qbBKzP1m+JGeYxjZlY37KFhkjwE2cVaHfSr2BSdg6IaiWTpl8w5xtOsepTqNViFHffPU1qv4qIkj08PFjcIC2l2Rg9KiJKJsr4DhzutaZCeBRddvH3YTtX1HLJAQFyTTOxqoCYCSNcB+zqUQwR/mMN3aTIgLYJZdZ0zVp5mEjFQagRk4q6yCILzEE0E8vVvpwj/f27A8ntkGqTsOWUId+xR5wMzOkDBSeg+jGlWhv0lQpN8oLm2BE/o9h5DOCjnbmDn6S+TTNzp32dI95xFYPxQTVyOfTiqzgH/oEugrwFH3knS4+8ckXxdCKO9vzVMSYpqDIP49e39dpBAVjN+bQnJ74s0VZk6+2GCoG4VFE134tvO9j0fSqZtiAa3IX49lCXabf2wucR5iMKatb3GZdAV9mIR/OJtFfin0U2BnbI7EPQfLg//iupK8m1rRnSUv4iqvMcIpU8Jfs1VQrGnCgtM4dlBWxGNF0/D30E740or+R7UmvGoVs+AtZYaUotKRV0ui+tY4AuQs3bVvGWOx0SXFFSlOmsqvNGsP33YUav3HD6PYCGolPIjurtv+O5PZ4Qv6zKPdGc72ZTppO5Afj2y0z+xb+dgeitu5RkcKya4mRCieqT97xK0TK0uoPR0GHAkLss1f/H/oZ0swbW1Z1AVBJ2H/Mvw6WAjZ0umtXdBn9mfIhVPBHPKHg/t5+Go4rHX3kMBeV6BKOg/+vviE5JnH6md3pQDLeiH41cFbY3GjgYXE4ljRO4SsEnjpEP8uACsOrw8mZP19xofC5Y6LcSHWhEA6YbUd1rk38KVPgpSemDxqXy6qyhuEXHMHwzX5K9KMzMwDeFMpx7eRmst7bX/fZfJCMW4scO7FPkqrqaqa3HOYCiNyJ3oEpFMIR/SzaCnqATmyPSu7KfuWFsH1wiPsTGda775aHSvB4HqiOG1LuYaBmpS0Vgy/SwiK4bKwzd08kLVrWwKYEztZJIm0AnZhupaOhHstx097CFhor2m3EJ8PUIGLXE1ApKn+bHfLuZsALkYTW3LIpSxulpgJjTeqGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":269,"cache_read_input_tokens":133736,"cache_creation":{"ephemeral_5m_input_tokens":269,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"52fb834b-1a24-418f-ba3e-2be7ddeedc6d"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMyxkxea3guLy9g1sGE","type":"message","role":"assistant","content":[{"type":"text","text":"Le texte est complet mais `paragraphe()` ajoute un « … » parasite en fin — je passe à max_lignes=3 pour l'éviter."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":269,"cache_read_input_tokens":133736,"cache_creation":{"ephemeral_5m_input_tokens":269,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"bd7a66fb-8b91-4de9-b702-cce44f9c1699"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMyxkxea3guLy9g1sGE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016HtZXVZrmoWE8X1d3ZB2P3","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=2)\n y -= 6","new_string":" PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=3)\n y -= 6"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":269,"cache_read_input_tokens":133736,"cache_creation":{"ephemeral_5m_input_tokens":269,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"655e5a80-536a-468c-b257-a01313fb30c2"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016HtZXVZrmoWE8X1d3ZB2P3","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"5ed857f9-0021-4034-809c-8ce0803538d9","timestamp":"2026-08-24T22:28:48.417Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=2)\n y -= 6","newString":" PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=3)\n y -= 6","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, coût réel mensuel,\n# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,\n# gestionnaire (fiche Google + avis synchronisés), registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, score hiver, quartier,\n# à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n y = s.paragraphe(M, y - 2, \"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\",\n PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n y = s.paragraphe(M, y, \" · \".join(stats_g), PAGE_W - 2 * M,\n taille=8, couleur=INK2, interligne=10,\n max_lignes=2)\n y -= 2\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n y = s.paragraphe(M, y - 2, f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur \"\n f\"les {avis_g.get('n', 0)} avis synchronisés dans \"\n \"la base Lou-Ka — échantillon des plus récents, \"\n \"pas une lecture humaine.\",\n PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=2)\n y -= 6\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":862,"oldLines":7,"newStart":862,"newLines":7,"lines":[" \"la base Lou-Ka — échantillon des plus récents, \""," \"pas une lecture humaine.\","," PAGE_W - 2 * M, taille=6.5, couleur=INK3,","- interligne=8.5, max_lignes=2)","+ interligne=8.5, max_lignes=3)"," y -= 6"," "," # --- Registre des loyers (déclarations citoyennes à proximité)"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNMyxkxea3guLy9g1sGE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RYSzNzTpdxnzJA6XKNthAP","name":"Edit","input":{"replace_all":false,"file_path":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","old_string":" PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)","new_string":" PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=3)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":269,"cache_read_input_tokens":133736,"cache_creation":{"ephemeral_5m_input_tokens":269,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"76710cbc-0bfc-4cd9-8b46-1dabcd485886"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RYSzNzTpdxnzJA6XKNthAP","type":"tool_result","content":"The file /Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py has been updated successfully."}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"84dca6a2-8661-4e6e-b4a3-87d1de9293d1","timestamp":"2026-08-24T22:28:50.527Z","tool_use_result":{"filePath":"/Users/simon-pierreboucher/apps/lou-ka/louka/pdfgen.py","oldString":" PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)","newString":" PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=3)","originalFile":"# -----------------------------------------------------------------------------\n# Lou-Ka — Agrégateur de logements à louer (province de Québec)\n# Auteur : Simon-Pierre Boucher — contact@spboucher.ai\n# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka\n# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la\n# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme),\n# chips, description structurée, inclusions, coût réel mensuel,\n# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble,\n# gestionnaire (fiche Google + avis synchronisés), registre des loyers,\n# historique TAL, inondation, qualité de l'air, coût d'électricité,\n# commerces/transport, essence, KA Scores, score hiver, quartier,\n# à proximité, QR\n# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme\n# des loyers, types, villes, gestionnaires)\n# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00,\n# bleu vif #2258ff, gris pâle #f2f2f2. Polices de base (Helvetica) stylées\n# par la mise en page — aucune dépendance de police externe.\n# -----------------------------------------------------------------------------\nfrom __future__ import annotations\n\nimport io\nimport json\nimport math\nimport time\n\nimport qrcode\nimport requests\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.lib.utils import ImageReader\nfrom reportlab.pdfgen import canvas as rl_canvas\n\nfrom . import db\n\n# --- palette Lou-Ka (identité officielle) ------------------------------------\nPAPER = HexColor(\"#ffffff\")\nSURFACE = HexColor(\"#ffffff\")\nINK = HexColor(\"#0b1330\") # bleu marine profond\nINK2 = HexColor(\"#3e4560\")\nINK3 = HexColor(\"#8a90a5\")\nGREEN = HexColor(\"#2258ff\") # accent secondaire : bleu vif\nGREEN_DEEP = HexColor(\"#0b1330\")\nLIME = HexColor(\"#ff6a00\") # accent d'action : orange vif\nLIME_SOFT = HexColor(\"#fff1e6\")\nAMBER = HexColor(\"#ff6a00\")\nAMBER_SOFT = HexColor(\"#fff1e6\")\n\nPAGE_W, PAGE_H = letter\nM = 40 # marge\nIMG_TIMEOUT = 6\nUA = \"LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)\"\n\nPOI_LABELS = {\n \"epicerie\": \"Épicerie\", \"depanneur\": \"Dépanneur\", \"pharmacie\": \"Pharmacie\",\n \"ecole\": \"École\", \"garderie\": \"Garderie\", \"parc\": \"Parc\", \"bus\": \"Arrêt de bus\",\n \"metro\": \"Métro\", \"gym\": \"Gym\", \"cafe\": \"Café\", \"clinique\": \"Clinique\",\n \"hopital\": \"Hôpital\", \"bibliotheque\": \"Bibliothèque\",\n}\nPROX_LABELS = [\n (\"prox_epicerie\", \"Épiceries\"), (\"prox_transport\", \"Transport en commun\"),\n (\"prox_parc\", \"Parcs\"), (\"prox_ecole_prim\", \"Écoles primaires\"),\n (\"prox_sante\", \"Soins de santé\"), (\"prox_pharmacie\", \"Pharmacies\"),\n]\n\nNBSP = \" \"\n\n\ndef _fmt_money(v) -> str:\n return f\"{v:,.0f}\".replace(\",\", f\"{NBSP}\") + f\"{NBSP}$\" if v is not None else \"—\"\n\n\ndef _fmt_dist(m_: float) -> str:\n return (f\"{round(m_ / 10) * 10}{NBSP}m\" if m_ < 1000\n else f\"{m_ / 1000:.1f}\".replace(\".\", \",\") + f\"{NBSP}km\")\n\n\ndef _marche(m_: float) -> str:\n return f\"≈{NBSP}{max(1, round(m_ * 1.3 / 80))}{NBSP}min à pied\"\n\n\ndef _fetch_image(url: str) -> ImageReader | None:\n try:\n r = requests.get(url, timeout=IMG_TIMEOUT, headers={\"User-Agent\": UA})\n r.raise_for_status()\n raw = r.content\n try:\n # les photos source dépassent parfois 5 Mo chacune : sans\n # redimensionnement le PDF grimpe à 30 Mo\n from PIL import Image\n im = Image.open(io.BytesIO(raw))\n im.thumbnail((1200, 1200))\n buf = io.BytesIO()\n im.convert(\"RGB\").save(buf, \"JPEG\", quality=82)\n buf.seek(0)\n return ImageReader(buf)\n except Exception:\n return ImageReader(io.BytesIO(raw))\n except Exception:\n return None\n\n\nclass _Style:\n \"\"\"Petits composants du style « éditorial sharp » sur un canvas.\"\"\"\n\n def __init__(self, c: rl_canvas.Canvas):\n self.c = c\n\n def fond(self):\n self.c.setFillColor(PAPER)\n self.c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)\n\n def logo(self, x: float, y: float, taille: float = 22):\n \"\"\"Wordmark Lou·Ka : « Lou » encre + boîte encre avec « Ka » lime.\"\"\"\n c = self.c\n c.setFont(\"Helvetica-Bold\", taille)\n c.setFillColor(INK)\n c.drawString(x, y, \"Lou\")\n w = c.stringWidth(\"Lou\", \"Helvetica-Bold\", taille)\n bw = c.stringWidth(\"Ka\", \"Helvetica-Bold\", taille) + 8\n c.saveState()\n c.translate(x + w + 3 + bw / 2, y + taille * 0.32)\n c.rotate(-3)\n c.setFillColor(INK)\n c.roundRect(-bw / 2, -taille * 0.62, bw, taille * 1.15, 4, stroke=0, fill=1)\n c.setFillColor(LIME)\n c.drawCentredString(0, -taille * 0.30, \"Ka\")\n c.restoreState()\n return x + w + 6 + bw\n\n def entete(self, titre: str):\n c = self.c\n self.logo(M, PAGE_H - M - 16)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK3)\n c.drawRightString(PAGE_W - M, PAGE_H - M - 6,\n titre.upper())\n c.drawRightString(PAGE_W - M, PAGE_H - M - 16,\n time.strftime(\"GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n c.setStrokeColor(INK)\n c.setLineWidth(2)\n c.line(M, PAGE_H - M - 26, PAGE_W - M, PAGE_H - M - 26)\n return PAGE_H - M - 40 # y de départ du contenu\n\n def pied(self, texte: str, page: int | None = None):\n c = self.c\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, M + 16, PAGE_W - M, M + 16)\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, M + 6, texte[:150])\n if page:\n c.setFont(\"Courier-Bold\", 7)\n c.drawRightString(PAGE_W - M, M + 6, f\"P.{page}\")\n\n def pilule(self, x: float, y: float, texte: str, fg=LIME, bg=INK,\n stroke=None, taille: float = 8) -> float:\n \"\"\"Chip arrondie ; retourne le x suivant.\"\"\"\n c = self.c\n w = c.stringWidth(texte, \"Helvetica-Bold\", taille) + 14\n c.setFillColor(bg)\n if stroke:\n c.setStrokeColor(stroke)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, taille + 9, (taille + 9) / 2,\n stroke=1 if stroke else 0, fill=1)\n c.setFillColor(fg)\n c.setFont(\"Helvetica-Bold\", taille)\n c.drawString(x + 7, y + 5, texte)\n return x + w + 6\n\n def titre_section(self, y: float, texte: str) -> float:\n c = self.c\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(M, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n w = c.stringWidth(texte, \"Helvetica-Bold\", 12.5)\n c.line(M, y - 4, M + w, y - 4)\n return y - 18\n\n def paragraphe(self, x: float, y: float, texte: str, largeur: float,\n taille: float = 8.5, couleur=INK2, interligne: float = 11.5,\n max_lignes: int = 100) -> float:\n \"\"\"Texte multi-lignes avec découpe aux mots ; retourne le y suivant.\"\"\"\n c = self.c\n c.setFont(\"Helvetica\", taille)\n c.setFillColor(couleur)\n mots = texte.split()\n ligne = \"\"\n n = 0\n for mot in mots:\n essai = (ligne + \" \" + mot).strip()\n if c.stringWidth(essai, \"Helvetica\", taille) <= largeur:\n ligne = essai\n else:\n c.drawString(x, y, ligne)\n y -= interligne\n n += 1\n ligne = mot\n if n >= max_lignes - 1:\n ligne += \" …\"\n break\n if ligne:\n c.drawString(x, y, ligne)\n y -= interligne\n return y\n\n def tuile(self, x: float, y: float, w: float, h: float,\n val: str, lab: str):\n \"\"\"Tuile KPI blanche à ombre décalée (x, y = coin inférieur gauche).\"\"\"\n c = self.c\n c.setFillColor(INK)\n c.roundRect(x + 2.5, y - 2.5, w, h, 5, stroke=0, fill=1)\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.roundRect(x, y, w, h, 5, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 12)\n c.setFillColor(INK)\n c.drawString(x + 8, y + h - 20, str(val))\n c.setFont(\"Courier\", 6.2)\n c.setFillColor(INK3)\n c.drawString(x + 8, y + 7, lab.upper()[: max(4, int(w / 4.2))])\n\n\n# ---------------------------------------------------------------------------\n# Fiche de propriété\n# ---------------------------------------------------------------------------\n\n_MOIS = [\"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\",\n \"août\", \"septembre\", \"octobre\", \"novembre\", \"décembre\"]\n\n\ndef _fmt_mois(d: str | None) -> str:\n \"\"\"« 2017-11-04 » → « novembre 2017 » (sinon la valeur telle quelle).\"\"\"\n if not d:\n return \"—\"\n try:\n an, mois = d[:7].split(\"-\")\n return f\"{_MOIS[int(mois) - 1]} {an}\"\n except Exception:\n return d\n\n\ndef _fmt_ts(ts, jour: bool = False) -> str:\n \"\"\"Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».\"\"\"\n if not ts:\n return \"—\"\n t = time.localtime(ts)\n mois = f\"{_MOIS[t.tm_mon - 1]} {t.tm_year}\"\n return f\"{t.tm_mday} {mois}\" if jour else mois\n\n\n_VERDICTS = {\n \"sous\": (\"Sous le marché\", GREEN_DEEP, LIME_SOFT, GREEN),\n \"marche\": (\"Dans le marché\", INK2, SURFACE, INK),\n \"sur\": (\"Au-dessus du marché\", HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER),\n}\n\n_PIED_FICHE = (\"Données de l'annonce publiée par le gestionnaire — Lou-Ka est \"\n \"un agrégateur indépendant. Enrichissements (analyse de prix, \"\n \"registre, TAL, environnement) fournis à titre indicatif, sans \"\n \"valeur juridique.\")\n\ndef fiche_pdf(uid: str) -> bytes | None:\n \"\"\"PDF de la fiche du logement `uid` — reflète TOUS les blocs de la fiche\n web : prix + analyse Lou-Ka, photos, description, inclusions, registre des\n loyers, historique TAL, risque d'inondation, qualité de l'air, coût\n d'électricité, commerces/transport, essence, KA Scores, quartier,\n à proximité, références + QR. None si introuvable.\"\"\"\n con = db.connect()\n row = con.execute(\"SELECT * FROM listings WHERE uid=?\", (uid,)).fetchone()\n if row is None:\n con.close()\n return None\n l = dict(row)\n l[\"amenities\"] = json.loads(l.get(\"amenities\") or \"[]\")\n l[\"images\"] = json.loads(l.get(\"images\") or \"[]\")\n l[\"details\"] = json.loads(l.get(\"details\") or \"{}\")\n l[\"digest\"] = json.loads(l[\"digest\"]) if l.get(\"digest\") else None\n poi = []\n quartier = None\n if l.get(\"lat\") is not None:\n key = f\"{round(l['lat'], 4)},{round(l['lng'], 4)}\"\n pr = con.execute(\"SELECT pois FROM poi_cache WHERE coord_key=?\", (key,)).fetchone()\n poi = json.loads(pr[\"pois\"]) if pr else []\n from . import quartier as qmod\n dauid = l.get(\"dauid\")\n quartier = qmod.fiche_quartier(l[\"lat\"], l[\"lng\"], l.get(\"city\") or \"\",\n dauid if dauid and dauid != \"hors-zone\" else None)\n reg = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n src_name = next((s[\"name\"] for s in reg if s[\"id\"] == l[\"source\"]), l[\"source\"])\n kasc = None\n if l.get(\"coord_key\"):\n kr = con.execute(\"SELECT walk, transit, bike, calme, services,\"\n \" global AS glob FROM kascores WHERE coord_key=?\",\n (l[\"coord_key\"],)).fetchone()\n if kr is not None:\n kasc = dict(kr)\n price_history = [dict(r) for r in con.execute(\n \"SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts DESC LIMIT 6\",\n (uid,)).fetchall()]\n con.close()\n\n # --- enrichissements de la fiche (mêmes modules que les blocs du site) ---\n lat, lng = l.get(\"lat\"), l.get(\"lng\")\n geo = lat is not None and lng is not None\n\n def _safe(fn, *a, **k):\n try:\n return fn(*a, **k)\n except Exception:\n return None\n\n from . import air as air_mod\n from . import commerces as com_mod\n from . import fairvalue as fv_mod\n from . import gaz as gaz_mod\n from . import hydro as hydro_mod\n from . import inondation as inon_mod\n from . import rdl as rdl_mod\n from . import tal as tal_mod\n fv = _safe(fv_mod.explain, uid)\n rdl_d = _safe(rdl_mod.nearby, lat, lng) if geo else None\n tal_d = (_safe(tal_mod.lookup, l[\"address\"], l.get(\"city\"))\n if l.get(\"address\") else None)\n inon = _safe(inon_mod.lookup, lat, lng) if geo else None\n air_d = _safe(air_mod.lookup, lat, lng) if geo else None\n gaz_d = _safe(gaz_mod.nearby, lat, lng) if geo else None\n com = _safe(com_mod.nearby, lat, lng) if geo else None\n hyd = _safe(hydro_mod.estimate, adresse=l.get(\"address\") or \"\",\n uid=uid, lat=lat, lng=lng, solve=False)\n # dossier d'intelligence locative (mêmes modules que les blocs du site)\n from . import building as bld_mod\n from . import coutreel as cr_mod\n from . import history as hist_mod\n from . import hiver as hiv_mod\n from . import managers as mgr_mod\n from . import recycled as rec_mod\n cout = _safe(cr_mod.compute, uid)\n histo = _safe(hist_mod.timeline, uid)\n recyc = _safe(rec_mod.lookup, uid)\n imm = (_safe(bld_mod.fiche, l[\"building_key\"])\n if l.get(\"building_key\") else None)\n hiv = _safe(hiv_mod.score, lat, lng) if geo else None\n gest = _safe(mgr_mod.fiche, l[\"source\"])\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(f\"Lou-Ka — {l.get('title') or l.get('address')}\")\n s = _Style(c)\n s.fond()\n y = s.entete(\"Fiche logement\")\n page = [1]\n\n def saut_page() -> float:\n s.pied(_PIED_FICHE, page[0])\n c.showPage()\n page[0] += 1\n s.fond()\n return s.entete(\"Fiche logement · suite\")\n\n def place(y_: float, besoin: float) -> float:\n \"\"\"Saute de page si `besoin` points ne tiennent plus sous y_.\"\"\"\n return saut_page() if (y_ - besoin) < M + 34 else y_\n\n # --- bloc prix + badge marché (verdict du modèle Lou-Ka en priorité)\n prix_txt = (_fmt_money(l[\"price\"]) + f\"{NBSP}/ mois\") if l.get(\"price\") \\\n else (l.get(\"price_label\") or \"Prix sur demande\")\n c.setFont(\"Helvetica-Bold\", 30)\n c.setFillColor(INK)\n c.drawString(M, y - 24, prix_txt)\n loyer_secteur = (quartier or {}).get(\"demographie\", {}).get(\"loyer_moyen\") \\\n if quartier else None\n bx = M + c.stringWidth(prix_txt, \"Helvetica-Bold\", 30) + 14\n if l.get(\"price\") and fv and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(bx, y - 22, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n elif l.get(\"price\") and loyer_secteur:\n delta = (l[\"price\"] - loyer_secteur) / loyer_secteur\n pct = f\"{'+' if delta > 0 else '−'}{abs(round(delta * 100))}{NBSP}%\"\n if delta <= -0.15:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Bon deal\", GREEN_DEEP, LIME_SOFT, GREEN)\n elif delta <= 0.10:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Dans le marché\", INK2, SURFACE, INK)\n else:\n s.pilule(bx, y - 22, f\"{pct} vs secteur · Au-dessus du marché\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n y -= 44\n\n # --- titre + adresse\n c.setFont(\"Helvetica-Bold\", 13)\n c.setFillColor(INK)\n c.drawString(M, y, (l.get(\"title\") or l.get(\"address\") or \"\")[:80])\n y -= 14\n c.setFont(\"Helvetica\", 9)\n c.setFillColor(INK2)\n loc = \" · \".join(x for x in (l.get(\"address\"), l.get(\"sector\"), l.get(\"city\"))\n if x and x != l.get(\"title\"))\n c.drawString(M, y, loc[:110])\n y -= 20\n\n # --- chips clés\n x = M\n chips = []\n if l.get(\"unit_type\"):\n chips.append(l[\"unit_type\"])\n ad = l.get(\"availability_date\")\n if ad == \"now\":\n chips.append(\"Libre maintenant\")\n elif ad:\n chips.append(f\"Dispo {ad}\")\n if l.get(\"area_sqft\"):\n chips.append(f\"{round(l['area_sqft'])}{NBSP}pi²\")\n if l.get(\"furnished\"):\n chips.append(\"Meublé\")\n if l.get(\"pets\"):\n chips.append({\"oui\": \"Animaux acceptés\", \"non\": \"Animaux refusés\",\n \"conditions\": \"Animaux sous conditions\"}.get(l[\"pets\"], l[\"pets\"]))\n if l[\"details\"].get(\"floor\") is not None:\n chips.append(f\"{l['details']['floor']}e étage\")\n for chip in chips[:7]:\n x = s.pilule(x, y - 6, chip)\n y -= 30\n\n # --- photos : 1 grande + 3 vignettes\n imgs = [im for im in (_fetch_image(u) for u in l[\"images\"][:4]) if im]\n if imgs:\n big_h = 170\n big_w = big_h * 16 / 10.5\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n try:\n c.drawImage(imgs[0], M, y - big_h, big_w, big_h,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n except Exception:\n pass\n c.rect(M, y - big_h, big_w, big_h, stroke=1, fill=0)\n tx = M + big_w + 10\n th = (big_h - 16) / 3\n for i, im in enumerate(imgs[1:4]):\n ty = y - (i + 1) * th - i * 8\n try:\n c.drawImage(im, tx, ty, PAGE_W - M - tx, th,\n preserveAspectRatio=True, anchor=\"c\", mask=\"auto\")\n c.rect(tx, ty, PAGE_W - M - tx, th, stroke=1, fill=0)\n except Exception:\n pass\n y -= big_h + 18\n\n # --- « En bref » (digest) sur bandeau lime\n dg = l.get(\"digest\")\n if dg and dg.get(\"en_bref\"):\n c.setFillColor(LIME_SOFT)\n c.setStrokeColor(GREEN)\n c.setLineWidth(1.2)\n c.roundRect(M, y - 40, PAGE_W - 2 * M, 44, 6, stroke=1, fill=1)\n s.paragraphe(M + 10, y - 8, \"EN BREF — \" + dg[\"en_bref\"],\n PAGE_W - 2 * M - 20, taille=8.5, couleur=GREEN_DEEP,\n max_lignes=3)\n y -= 54\n\n # --- description (2-3 sections max) | inclusions en colonne droite\n col_g = PAGE_W * 0.56 - M\n y_desc = s.titre_section(y, \"Description\")\n if dg and dg.get(\"sections\"):\n for sec in dg[\"sections\"][:3]:\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y_desc, sec[\"titre\"][:60])\n y_desc -= 12\n y_desc = s.paragraphe(M, y_desc, sec[\"texte\"], col_g, max_lignes=6)\n y_desc -= 4\n if y_desc < 150:\n break\n elif l.get(\"description\"):\n y_desc = s.paragraphe(M, y_desc, l[\"description\"], col_g, max_lignes=16)\n\n xr = M + col_g + 24\n y_incl = s_titre_droite(c, y, xr, \"Inclusions et commodités\")\n d = l[\"details\"]\n badges = []\n inc = d.get(\"inclusions\") or {}\n for k, lab in ((\"heating\", \"Chauffage inclus\"), (\"electricity\", \"Électricité incluse\"),\n (\"hot_water\", \"Eau chaude incluse\"), (\"internet\", \"Internet inclus\")):\n if inc.get(k):\n badges.append(lab)\n app = d.get(\"appliances\") or {}\n if app.get(\"dishwasher\"):\n badges.append(\"Lave-vaisselle\")\n if app.get(\"washer_dryer\"):\n badges.append(\"Laveuse-sécheuse\")\n for k, lab in ((\"ac\", \"Climatisation\"), (\"elevator\", \"Ascenseur\"),\n (\"balcony\", \"Balcon\"), (\"pool\", \"Piscine\"), (\"gym\", \"Gym\"),\n (\"laundry\", \"Buanderie\"), (\"storage\", \"Rangement\")):\n if d.get(k):\n badges.append(lab)\n if (d.get(\"parking\") or {}).get(\"available\"):\n badges.append(\"Stationnement\")\n if l.get(\"furnished\"):\n badges.append(\"Meublé\")\n c.setFont(\"Helvetica\", 8.5)\n for b in badges[:12]:\n c.setFillColor(GREEN)\n c.drawString(xr, y_incl, \"✓\")\n c.setFillColor(INK2)\n c.drawString(xr + 12, y_incl, b)\n y_incl -= 12\n autres = [a for a in l[\"amenities\"]\n if not any(b.lower() in a.lower() or a.lower() in b.lower() for b in badges)]\n for a in autres[: max(0, 14 - len(badges))]:\n c.setFillColor(INK3)\n c.drawString(xr + 12, y_incl, a[:38])\n y_incl -= 11\n\n y = min(y_desc, y_incl) - 16\n\n # --- Analyse de prix Lou-Ka (juste valeur, fourchette, distribution)\n if fv and fv.get(\"fv\"):\n hist = fv.get(\"histogram\") or []\n y = place(y, 168 if hist else 92)\n y = s.titre_section(y, \"Analyse de prix Lou-Ka\")\n jv = _fmt_money(round(fv[\"fv\"]))\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n c.drawString(M, y - 10, jv)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth(jv, \"Helvetica-Bold\", 17) + 8, y - 10,\n \"/ mois — juste valeur estimée par le modèle Lou-Ka\")\n meta = []\n if fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n meta.append(f\"fourchette {_fmt_money(round(fv['fv_low']))} – \"\n f\"{_fmt_money(round(fv['fv_high']))}\")\n if fv.get(\"confidence\"):\n meta.append(f\"confiance {fv['confidence']}\")\n if fv.get(\"comps\"):\n meta.append(f\"{fv['comps']} comparables\")\n if fv.get(\"segment_n\"):\n meta.append(f\"segment de {fv['segment_n']} annonces\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y - 24, \" · \".join(meta)[:115])\n if l.get(\"price\") and fv.get(\"verdict\") in _VERDICTS:\n dev = fv.get(\"deviation\") or 0\n pct = f\"{'+' if dev > 0 else '−'}{abs(round(dev * 100))}{NBSP}%\"\n lab, fg, bg, stk = _VERDICTS[fv[\"verdict\"]]\n s.pilule(M, y - 48, f\"{pct} vs juste valeur · {lab}\", fg, bg, stk)\n y -= 58\n if hist:\n ch_h = 40\n lo, hi = hist[0][\"x0\"], hist[-1][\"x1\"]\n larg = PAGE_W - 2 * M\n base = y - ch_h - 6\n if hi > lo and fv.get(\"fv_low\") and fv.get(\"fv_high\"):\n bx0 = M + larg * max(0.0, (fv[\"fv_low\"] - lo) / (hi - lo))\n bx1 = M + larg * min(1.0, (fv[\"fv_high\"] - lo) / (hi - lo))\n c.setFillColor(LIME_SOFT)\n c.rect(bx0, base, max(2, bx1 - bx0), ch_h, stroke=0, fill=1)\n max_n = max((h[\"n\"] for h in hist), default=1) or 1\n bw = larg / len(hist)\n c.setFillColor(GREEN)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"n\"] / max_n\n if bh > 0:\n c.rect(M + i * bw + 1, base, bw - 2, max(0.6, bh),\n stroke=0, fill=1)\n if hi > lo and l.get(\"price\"):\n px = M + larg * (min(max(l[\"price\"], lo), hi) - lo) / (hi - lo)\n c.setStrokeColor(LIME)\n c.setLineWidth(2)\n c.line(px, base - 3, px, base + ch_h + 3)\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(LIME)\n c.drawString(min(max(px - 28, M), PAGE_W - M - 92),\n base + ch_h + 6,\n f\"CE LOYER · {_fmt_money(l['price'])}\")\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, base - 9, _fmt_money(lo))\n c.drawRightString(PAGE_W - M, base - 9,\n _fmt_money(hi) + \" — distribution des loyers du \"\n \"segment de référence\")\n y = base - 20\n ph = [p for p in price_history if p.get(\"price\")]\n if len(ph) >= 2 and ph[0][\"price\"] != ph[-1][\"price\"]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n chn = \" → \".join(_fmt_money(p[\"price\"]) for p in reversed(ph[:4]))\n c.drawString(M, y - 2, f\"Historique de prix de l'annonce : {chn}\")\n y -= 16\n\n # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement)\n if cout and cout.get(\"loyer\") is not None:\n lignes_cr = cout.get(\"lignes\") or []\n y = place(y, 58 + len(lignes_cr) * 12 + 46)\n y = s.titre_section(y, \"Coût réel mensuel\")\n stat_cr = {\"included\": (\"INCLUS\", GREEN), \"observed\": (\"OBSERVÉ\", INK),\n \"estimated\": (\"ESTIMÉ\", LIME), \"unknown\": (\"INCONNU\", INK3)}\n for li in lignes_cr:\n lab, col = stat_cr.get(li.get(\"statut\"), stat_cr[\"unknown\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, (li.get(\"poste\") or \"\")[:46])\n c.setFont(\"Courier-Bold\", 6.5)\n c.setFillColor(col)\n c.drawString(M + 230, y, lab)\n m_ = li.get(\"montant\")\n if m_ is not None and m_ > 0:\n mtx = _fmt_money(m_)\n elif m_ == 0 and li.get(\"statut\") == \"included\":\n mtx = f\"0{NBSP}$\"\n else:\n mtx = \"—\"\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, mtx)\n y -= 12\n if cout.get(\"total_estime\") is not None:\n c.setStrokeColor(INK)\n c.setLineWidth(1.2)\n c.line(M, y + 8, PAGE_W - M, y + 8)\n c.setFont(\"Helvetica-Bold\", 10)\n c.setFillColor(INK)\n c.drawString(M, y - 4, \"Total estimé\")\n tot = f\"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois\"\n if cout.get(\"annuel_estime\") is not None:\n tot += f\" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an\"\n c.drawRightString(PAGE_W - M, y - 4, tot)\n y -= 18\n if cout.get(\"postes_inconnus\"):\n y = s.paragraphe(M, y - 2, \"Postes non chiffrables avec les données \"\n \"publiées : \"\n + \", \".join(cout[\"postes_inconnus\"]).lower()\n + \" — le total réel peut être plus élevé.\",\n PAGE_W - 2 * M, taille=7.5,\n couleur=HexColor(\"#8a5a12\"), interligne=9.5,\n max_lignes=2)\n pi2_d = cout.get(\"pi2\")\n if pi2_d and pi2_d.get(\"valeur\") is not None:\n v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")\n suite = f\"{v}{NBSP}$/pi²\"\n if pi2_d.get(\"percentile_secteur\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_secteur']}\"\n f\"{NBSP}% des {pi2_d.get('n_secteur')} comparables du \"\n \"secteur (~2 km)\")\n elif pi2_d.get(\"percentile_ville\") is not None:\n suite += (f\" — moins cher que {100 - pi2_d['percentile_ville']}\"\n f\"{NBSP}% des {pi2_d.get('n_ville')} comparables \"\n f\"({pi2_d.get('portee_ville', '')})\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, suite[:120])\n y -= 12\n y -= 8\n\n # --- Historique Lou-Ka (observations réelles des synchronisations)\n if histo:\n tl = (histo.get(\"timeline\") or [])[:8]\n matches = ((recyc or {}).get(\"matches\") or [])[:2]\n y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18)\n y = s.titre_section(y, \"Historique Lou-Ka\")\n entete_h = (f\"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}\"\n f\" · {histo.get('jours_en_ligne', 0)} jour\"\n f\"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne\"\n f\" · {histo.get('modifications', 0)} modification\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée\"\n f\"{'s' if (histo.get('modifications') or 0) > 1 else ''}\")\n pi_, pa_ = histo.get(\"prix_initial\"), histo.get(\"prix_actuel\")\n if pi_ is not None and pa_ is not None and pi_ != pa_:\n var_ = histo.get(\"variation\")\n entete_h += f\" · {_fmt_money(pi_)} → {_fmt_money(pa_)}\"\n if var_ is not None:\n entete_h += (f\" ({'+' if var_ > 0 else '−'}\"\n f\"{abs(round(var_ * 100))}{NBSP}%)\")\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, entete_h[:120])\n y -= 16\n ev_labels = {\"description\": \"Description modifiée\",\n \"superficie\": \"Superficie modifiée\",\n \"dispo\": \"Disponibilité modifiée\",\n \"inclusions\": \"Inclusions modifiées\",\n \"photos\": \"Photos modifiées\",\n \"disparition\": \"Annonce retirée\",\n \"reapparition\": \"Annonce republiée\"}\n if tl:\n for it in tl:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y, _fmt_ts(it.get(\"ts\"), jour=True).upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if it.get(\"type\") == \"prix\":\n if it.get(\"prix_avant\") is not None:\n sens = (\"baissé\" if it[\"prix_avant\"] > (it.get(\"prix\") or 0)\n else \"monté\")\n txt_ev = (f\"Prix {sens} de {_fmt_money(it['prix_avant'])} \"\n f\"à {_fmt_money(it.get('prix'))}\")\n else:\n txt_ev = f\"Premier prix observé : {_fmt_money(it.get('prix'))}\"\n else:\n txt_ev = ev_labels.get(it.get(\"type\"), it.get(\"type\") or \"\")\n c.drawString(M + 78, y, txt_ev[:95])\n y -= 11\n else:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, \"Aucune modification observée depuis la première \"\n \"synchronisation de cette annonce.\")\n y -= 11\n for m_ in matches:\n suite = f\"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)\"\n if m_.get(\"prix\") is not None:\n suite += f\" — affiché {_fmt_money(m_['prix'])}\"\n if m_.get(\"derniere_observation\"):\n suite += f\" jusqu'en {_fmt_ts(m_['derniere_observation'])}\"\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(HexColor(\"#8a5a12\"))\n c.drawString(M, y - 2, suite[:115])\n y -= 10\n c.setFont(\"Helvetica\", 7)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (\"signaux : \" + \" · \".join(m_.get(\"signaux\") or []))[:125])\n y -= 12\n y -= 8\n\n # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment)\n if imm and (imm.get(\"annonces_total\") or 0) >= 2:\n y = place(y, 128)\n y = s.titre_section(y, \"Passeport de l'immeuble\")\n tw = (PAGE_W - 2 * M - 30) / 4\n rot = imm.get(\"rotation\") or {}\n pres = imm.get(\"pression_loyers\")\n tuiles_im = [\n (str(imm[\"annonces_total\"]),\n f\"annonces obs. ({imm.get('annonces_actives', 0)} actives)\"),\n (f\"≥{NBSP}{imm.get('unites_estimees', 0)}\", \"unités estimées\"),\n (_fmt_money(imm.get(\"loyer_median\")), \"loyer médian (actives)\"),\n (f\"{imm['pi2_median']:.2f}\".replace(\".\", \",\") + f\"{NBSP}$\"\n if imm.get(\"pi2_median\") is not None else \"—\", \"médiane $/pi²\"),\n ]\n for i, (val, lab) in enumerate(tuiles_im):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 58\n par_cc = imm.get(\"loyer_median_par_cc\") or {}\n if par_cc:\n parts = [f\"{cc}{NBSP}ch. {_fmt_money(v)}\"\n for cc, v in sorted(par_cc.items())]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Loyer médian par nombre de chambres : \"\n + \" · \".join(parts))[:118])\n y -= 13\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Rotation des logements :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if rot.get(\"statut\") == \"calculated\":\n c.drawString(M + 118, y,\n (f\"{rot.get('classe', '')} — {rot.get('annonces_12m')} \"\n f\"annonces sur 12 mois pour ≥{NBSP}\"\n f\"{rot.get('unites_estimees')} unités\")[:100])\n else:\n c.drawString(M + 118, y, \"données insuffisantes\")\n y -= 12\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, \"Pression sur les loyers :\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if pres:\n v12 = pres.get(\"variation_12m\") or 0\n c.drawString(M + 118, y,\n (f\"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}\"\n f\"{NBSP}% sur 12 mois — médiane des prix d'entrée \"\n f\"{_fmt_money(pres.get('mediane_12_24m'))} → \"\n f\"{_fmt_money(pres.get('mediane_12m'))} \"\n f\"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)\")[:110])\n else:\n c.drawString(M + 118, y, \"échantillon insuffisant\")\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (f\"Immeuble suivi par Lou-Ka depuis \"\n f\"{_fmt_ts(imm.get('premiere_observation'))} — \"\n \"indicateurs calculés uniquement sur les annonces \"\n \"observées (pas un recensement du bâtiment).\")[:135])\n y -= 16\n\n # --- Qui gère ce logement? (fiche Google associée + avis synchronisés)\n g_ = (gest or {}).get(\"google_maps\")\n if gest and g_ and g_.get(\"statut\") != \"non_associe\":\n avis_g = gest.get(\"avis\") or {}\n dist_g = avis_g.get(\"distribution\") or {}\n tot_g = sum(dist_g.values()) if dist_g else 0\n y = place(y, 108 + (5 * 11 if tot_g else 0))\n y = s.titre_section(y, \"Qui gère ce logement?\")\n c.setFont(\"Helvetica-Bold\", 10.5)\n c.setFillColor(INK)\n c.drawString(M, y - 2, (gest.get(\"nom\") or \"\")[:60])\n n_act = gest.get(\"annonces_actives\") or 0\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + c.stringWidth((gest.get(\"nom\") or \"\")[:60],\n \"Helvetica-Bold\", 10.5) + 10, y - 2,\n f\"{n_act} annonce{'s' if n_act > 1 else ''} active\"\n f\"{'s' if n_act > 1 else ''} sur Lou-Ka\")\n y -= 16\n if g_.get(\"note\") is not None:\n note_t = f\"{g_['note']:.1f}\".replace(\".\", \",\")\n s.pilule(M, y - 10, f\"Note Google {note_t} / 5\", LIME, INK)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 110, y - 5,\n (f\"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche \"\n f\"« {g_.get('nom', '')} »\")[:90])\n y -= 22\n if tot_g:\n bw_g = 150.0\n for n_ in (5, 4, 3, 2, 1):\n cnt = dist_g.get(str(n_), 0)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{n_}*\")\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1)\n if cnt:\n c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3))\n c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK)\n c.drawString(M + 22 + bw_g + 6, y, str(cnt))\n y -= 11\n y -= 2\n stats_g = []\n if avis_g.get(\"moyenne_12m\") is not None:\n stats_g.append(f\"moyenne des 12 derniers mois : \"\n f\"{avis_g['moyenne_12m']:.1f}\".replace(\".\", \",\")\n + f\" ({avis_g.get('n_12m')} avis)\")\n if avis_g.get(\"tendance\"):\n stats_g.append(avis_g[\"tendance\"])\n if avis_g.get(\"plaintes_frequentes\"):\n stats_g.append(\"plaintes récurrentes : \"\n + \", \".join(avis_g[\"plaintes_frequentes\"][:4]))\n if stats_g:\n y = s.paragraphe(M, y, \" · \".join(stats_g), PAGE_W - 2 * M,\n taille=8, couleur=INK2, interligne=10,\n max_lignes=2)\n y -= 2\n conf_g = round((g_.get(\"confiance_association\") or 0) * 100)\n y = s.paragraphe(M, y - 2, f\"Fiche Google Maps associée automatiquement \"\n f\"(confiance {conf_g}{NBSP}%). Statistiques sur \"\n f\"les {avis_g.get('n', 0)} avis synchronisés dans \"\n \"la base Lou-Ka — échantillon des plus récents, \"\n \"pas une lecture humaine.\",\n PAGE_W - 2 * M, taille=6.5, couleur=INK3,\n interligne=8.5, max_lignes=3)\n y -= 6\n\n # --- Registre des loyers (déclarations citoyennes à proximité)\n if rdl_d and rdl_d.get(\"n\"):\n comp = rdl_d.get(\"items\") or []\n y = place(y, 128 + min(len(comp), 8) * 12)\n y = s.titre_section(y, \"Registre des loyers — déclarations citoyennes\")\n tw = (PAGE_W - 2 * M - 20) / 3\n med_rec = rdl_d.get(\"median_recent\")\n tuiles = [(str(rdl_d[\"n\"]), f\"loyers déclarés à <{rdl_d['radius_m']} m\"),\n (_fmt_money(rdl_d.get(\"median\")), \"médiane du secteur\"),\n (_fmt_money(med_rec) if med_rec else \"—\",\n f\"médiane depuis 2023 ({rdl_d.get('n_recent', 0)} décl.)\")]\n for i, (val, lab) in enumerate(tuiles):\n s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab)\n y -= 56\n rooms = rdl_d.get(\"by_rooms\") or {}\n if rooms:\n def _rk(k: str) -> float:\n try:\n return float(k)\n except ValueError:\n return 99.0\n parts = [f\"{k}{NBSP}ch. {_fmt_money(v['median'])} ({v['n']})\"\n for k, v in sorted(rooms.items(), key=lambda kv: _rk(kv[0]))]\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (\"Médianes par taille : \" + \" · \".join(parts))[:118])\n y -= 14\n if comp:\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"ADRESSE\", 0), (\"CH.\", 280), (\"DÉBUT DE BAIL\", 320),\n (\"DIST.\", 420), (\"LOYER\", 480)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 11\n for it in comp[:8]:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, (it.get(\"address\") or \"\")[:52])\n c.drawString(M + 280, y, str(it[\"rooms\"]) if it.get(\"rooms\") is not None else \"—\")\n c.drawString(M + 320, y, _fmt_mois(it.get(\"date\")))\n c.drawString(M + 420, y, _fmt_dist(it[\"dist_m\"]))\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M + 480, y, _fmt_money(it.get(\"price\")))\n y -= 12\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Loyers déclarés volontairement par des locataires \"\n \"au Registre des loyers (registre-des-loyers.ca) — \"\n \"données citoyennes non vérifiées.\")\n y -= 16\n\n # --- Historique au TAL (décisions SOQUIJ à l'adresse)\n if tal_d and tal_d.get(\"status\") in (\"ok\", \"pending\"):\n decs = (tal_d.get(\"decisions\") or [])[:6]\n y = place(y, 70 + len(decs) * 24)\n y = s.titre_section(y, \"Historique au Tribunal administratif du logement\")\n if tal_d[\"status\"] == \"pending\":\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 4, \"Adresse en file de vérification auprès des \"\n \"décisions publiées du TAL (résultat sur la \"\n \"fiche en ligne sous peu).\")\n y -= 18\n elif not decs:\n s.pilule(M, y - 12, \"Aucune décision trouvée\", GREEN_DEEP,\n LIME_SOFT, GREEN)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 130, y - 7, \"Aucune décision publiée du TAL n'a été \"\n \"repérée à cette adresse.\")\n y -= 26\n else:\n n = tal_d.get(\"n\") or len(decs)\n cl = tal_d.get(\"contre_locataire\") or 0\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M, y - 4,\n f\"{n} décision{'s' if n > 1 else ''} publiée\"\n f\"{'s' if n > 1 else ''} à cette adresse — \"\n f\"{cl} à l'initiative du propriétaire.\")\n y -= 20\n for dec in decs:\n qui = {\"locateur\": \"demande du propriétaire\",\n \"locataire\": \"demande du locataire\"}.get(\n dec.get(\"demandeur\") or \"\", \"demandeur inconnu\")\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, _fmt_mois(dec.get(\"date\")))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n suite = qui\n if dec.get(\"verdict\"):\n suite += f\" · demande {dec['verdict']}\"\n if dec.get(\"tags\"):\n suite += \" · \" + \", \".join(dec[\"tags\"][:3])\n c.drawString(M + 78, y, suite[:88])\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y, dec.get(\"citation\") or \"SOQUIJ\")\n y -= 12\n y -= 6\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, \"Décisions publiques diffusées par SOQUIJ, repérées \"\n \"par correspondance d'adresse — une décision peut \"\n \"concerner un autre logement du même immeuble.\")\n y -= 16\n\n # --- Risque d'inondation (BDZI)\n if inon:\n zones = (inon.get(\"zones\") or [])[:3]\n y = place(y, 56 + len(zones) * 12)\n y = s.titre_section(y, \"Risque d'inondation\")\n statut = inon.get(\"statut\")\n sev = {\"eleve\": \"récurrence élevée\", \"modere\": \"récurrence modérée\",\n \"present\": \"zone cartographiée\"}.get(inon.get(\"severite\") or \"\", \"\")\n if statut == \"en_zone\":\n s.pilule(M, y - 12, f\"En zone inondable ({sev})\",\n HexColor(\"#a12622\"), HexColor(\"#fde8e8\"), HexColor(\"#a12622\"))\n elif statut == \"a_proximite\":\n d0 = zones[0][\"distance_m\"] if zones else \"?\"\n s.pilule(M, y - 12, f\"Zone inondable à ~{d0} m\",\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER)\n elif statut == \"hors_zone\":\n s.pilule(M, y - 12, \"Hors des zones inondables cartographiées\",\n GREEN_DEEP, LIME_SOFT, GREEN)\n else:\n s.pilule(M, y - 12, \"Secteur non couvert par la cartographie BDZI\",\n INK2, SURFACE, INK)\n y -= 26\n for z in zones:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{z['type']} — à {z['distance_m']} m\"\n + (f\" (rapport {z['date_rapport']})\"\n if z.get(\"date_rapport\") else \"\"))\n y -= 12\n y -= 8\n\n # --- Qualité de l'air (RSQAQ)\n if air_d and air_d.get(\"station\"):\n mes = air_d.get(\"mesures\") or {}\n y = place(y, 44 + len(mes) * 11)\n y = s.titre_section(y, \"Qualité de l'air\")\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2,\n f\"Station {air_d['station']} ({air_d['ville']}), à \"\n + f\"{air_d['distance_km']}\".replace(\".\", \",\") + f\"{NBSP}km \"\n \"du logement — moyennes annuelles :\")\n y -= 16\n for pol, m_ in list(mes.items())[:5]:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, pol)\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n val = f\"{m_['moyenne']:.1f}\".replace(\".\", \",\") if m_.get(\"moyenne\") is not None else \"—\"\n txt = f\"{val}{NBSP}{m_.get('unite', '')} ({m_.get('annee', '')})\"\n if m_.get(\"ref\"):\n txt += f\" · référence {m_.get('ref_nom', '')} : {m_['ref']}\"\n c.drawString(M + 56, y, txt[:95])\n y -= 11\n y -= 8\n\n # --- Coût d'électricité (Hydro-Québec)\n if hyd and hyd.get(\"disponible\"):\n y = place(y, 52)\n y = s.titre_section(y, \"Coût d'électricité (Hydro-Québec)\")\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n mens = f\"≈{NBSP}{_fmt_money(round(hyd['cout_mensuel']))} / mois\"\n c.drawString(M, y - 8, mens)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n suite = f\"{_fmt_money(round(hyd['cout_annuel']))} / an\"\n if hyd.get(\"kwh_annuel\"):\n suite += f\" · {round(hyd['kwh_annuel']):,}\".replace(\",\", NBSP) + f\"{NBSP}kWh/an\"\n suite += \" — estimation officielle Hydro-Québec pour cette adresse.\"\n c.drawString(M + c.stringWidth(mens, \"Helvetica-Bold\", 15) + 10, y - 8, suite)\n y -= 26\n\n # --- Commerces et transport\n if com and (com.get(\"commerces\") or com.get(\"transit\")):\n entrees = ([(t[\"commerce\"], t[\"nom\"], t[\"dist_m\"])\n for t in (com.get(\"transit\") or [])[:4]]\n + [(i[\"commerce\"], i[\"nom\"], i[\"dist_m\"])\n for i in (com.get(\"commerces\") or [])[:10]])\n lignes = (len(entrees) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"Commerces et transport\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, (cat, nom, dist) in enumerate(entrees):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n nomc = nom if len(nom) <= 22 else nom[:21] + \"…\"\n c.drawString(px, py, f\"{cat} — {nomc}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py, _fmt_dist(dist))\n y -= lignes * 13 + 12\n\n # --- Essence à proximité\n if gaz_d and gaz_d.get(\"stations\"):\n sts = gaz_d[\"stations\"][:3]\n y = place(y, 40 + len(sts) * 11)\n y = s.titre_section(y, \"Essence à proximité\")\n if gaz_d.get(\"mediane_regulier\"):\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y - 2, \"Prix médian de l'essence régulière à <5 km : \"\n + f\"{gaz_d['mediane_regulier']}\".replace(\".\", \",\")\n + f\"{NBSP}¢/L\")\n y -= 14\n for st in sts:\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n reg_p = (f\"{st['regulier']}\".replace(\".\", \",\") + f\"{NBSP}¢/L\"\n if st.get(\"regulier\") else \"—\")\n c.drawString(M, y, f\"{st['nom']} — {_fmt_dist(st['dist_m'])} : {reg_p}\"\n + (\" · la moins chère\" if st.get(\"moins_chere\") else \"\"))\n y -= 11\n y -= 8\n\n # --- KA Scores (immeuble et environnement)\n if kasc and kasc.get(\"glob\") is not None:\n from . import kascores as ks_mod\n y = place(y, 92)\n y = s.titre_section(y, \"KA Scores\")\n paires = [(\"Global\", kasc.get(\"glob\")), (\"À pied\", kasc.get(\"walk\")),\n (\"Transport\", kasc.get(\"transit\")), (\"Vélo\", kasc.get(\"bike\")),\n (\"Calme\", kasc.get(\"calme\")), (\"Services\", kasc.get(\"services\"))]\n tw = (PAGE_W - 2 * M - 5 * 8) / 6\n for i, (lab, v) in enumerate(paires):\n val = \"—\" if v is None else str(round(v))\n sub = ks_mod.label(v) or \"\"\n s.tuile(M + i * (tw + 8), y - 46, tw, 42, val,\n f\"{lab} · {sub}\" if sub else lab)\n y -= 62\n\n # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C)\n if hiv:\n det_h = hiv.get(\"detail\") or []\n y = place(y, 52 + len(det_h) * 11 + 14)\n y = s.titre_section(y, \"Vie quotidienne en hiver\")\n c.setFont(\"Helvetica-Bold\", 17)\n c.setFillColor(INK)\n sc_t = str(hiv.get(\"score\", \"—\"))\n c.drawString(M, y - 8, sc_t)\n s.pilule(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 10, y - 12,\n (hiv.get(\"classe\") or \"\").upper())\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M + c.stringWidth(sc_t, \"Helvetica-Bold\", 17) + 105, y - 7,\n \"quotidien à pied, même à −20 °C\")\n y -= 26\n for cr_ in det_h:\n c.setFont(\"Helvetica-Bold\", 8)\n c.setFillColor(INK)\n c.drawString(M, y, (cr_.get(\"critere\") or \"\")[:24])\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n if cr_.get(\"distance_m\") is not None:\n val_h = f\"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied\"\n if cr_.get(\"nom\"):\n val_h += f\" ({cr_['nom']})\"\n else:\n val_h = cr_.get(\"note\") or \"—\"\n c.drawString(M + 90, y, val_h[:80])\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(cr_.get(\"score\", \"—\")))\n y -= 11\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y - 2, (hiv.get(\"methode\") or \"\")[:135])\n y -= 16\n\n # --- Le quartier\n if quartier:\n y = place(y, 270)\n y = s.titre_section(y, \"Le quartier\")\n demo = quartier.get(\"demographie\") or {}\n tuiles = [(\"Revenu médian\", _fmt_money(demo.get(\"revenu_median\"))),\n (\"Ménages locataires\", f\"{round(demo['pct_locataires'])}{NBSP}%\" if demo.get(\"pct_locataires\") is not None else \"—\"),\n (\"Loyer moyen secteur\", _fmt_money(demo.get(\"loyer_moyen\"))),\n (\"Âge médian\", f\"{round(demo['age_median'])} ans\" if demo.get(\"age_median\") is not None else \"—\"),\n (\"Français à la maison\", f\"{round(demo['pct_francais'])}{NBSP}%\" if demo.get(\"pct_francais\") is not None else \"—\"),\n (\"Diplôme universitaire\", f\"{round(demo['pct_univ'])}{NBSP}%\" if demo.get(\"pct_univ\") is not None else \"—\")]\n tw = (PAGE_W - 2 * M - 20) / 3\n for i, (lab, val) in enumerate(tuiles):\n s.tuile(M + (i % 3) * (tw + 10), y - 34 - (i // 3) * 44,\n tw, 38, val, lab)\n y -= 34 + 44 + 16\n\n prox = quartier.get(\"proximite\") or {}\n for k, lab in PROX_LABELS:\n if k not in prox:\n continue\n v = max(0.0, min(1.0, prox[k]))\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 130, PAGE_W - 2 * M - 160\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 7, 3.5, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * v, 7, 3.5, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, str(round(v * 100)))\n y -= 14\n y -= 4\n badges_q = []\n ch = quartier.get(\"chaleur\")\n if ch:\n if ch[\"classe\"] <= 3:\n badges_q.append((\"Îlot de fraîcheur\", GREEN_DEEP, LIME_SOFT, GREEN))\n elif ch[\"classe\"] >= 7:\n badges_q.append((f\"Îlot de chaleur (+{ch['ecart']:.1f} °C)\".replace(\".\", \",\"),\n HexColor(\"#8a5a12\"), AMBER_SOFT, AMBER))\n cr = quartier.get(\"crime\")\n if cr and cr.get(\"type\") == \"igc\" and cr.get(\"indice_canada\"):\n dlt = round(100 * (cr[\"indice\"] - cr[\"indice_canada\"]) / cr[\"indice_canada\"])\n badges_q.append((f\"Criminalité {abs(dlt)}{NBSP}% \"\n f\"{'sous' if dlt <= 0 else 'au-dessus de'} la moyenne canadienne\",\n INK2, SURFACE, INK))\n elif cr and cr.get(\"type\") == \"points\":\n badges_q.append((f\"{cr['douze_mois']} actes criminels à <500 m (12 mois)\",\n INK2, SURFACE, INK))\n x = M\n for txt, fg, bg, stk in badges_q:\n x = s.pilule(x, y - 8, txt, fg, bg, stk)\n y -= 30\n\n # --- À proximité (commodités du cache POI)\n if poi:\n lignes = (min(len(poi), 12) + 1) // 2\n y = place(y, 36 + lignes * 13)\n y = s.titre_section(y, \"À proximité\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, p in enumerate(poi[:12]):\n px = M if i % 2 == 0 else M + col_w + 10\n py = y - (i // 2) * 13\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n lab = POI_LABELS.get(p[\"cat\"], p[\"cat\"])\n nom_poi = p[\"name\"][:19] + \"…\" if len(p[\"name\"]) > 20 else p[\"name\"]\n c.drawString(px, py, f\"{lab} — {nom_poi}\")\n c.setFont(\"Courier-Bold\", 7.5)\n c.setFillColor(INK)\n c.drawRightString(px + col_w - 14, py,\n f\"{_fmt_dist(p['dist_m'])} · {_marche(p['dist_m'])}\")\n y -= lignes * 13 + 14\n\n # --- Références : QR + gestionnaire + source\n y = place(y, 120)\n y = s.titre_section(y, \"Références\")\n qr = qrcode.make(f\"https://www.lou-ka.com/logement/{uid}\", box_size=4, border=1)\n qb = io.BytesIO()\n qr.save(qb, format=\"PNG\")\n c.drawImage(ImageReader(io.BytesIO(qb.getvalue())), M, y - 74, 70, 70)\n c.setFont(\"Helvetica-Bold\", 9)\n c.setFillColor(INK)\n c.drawString(M + 82, y - 14, f\"Gestionnaire : {src_name}\")\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 28, \"Annonce originale :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 40, (l.get(\"url\") or \"\")[:90])\n c.setFillColor(INK2)\n c.drawString(M + 82, y - 56, \"Fiche à jour en ligne (scannez le code) :\")\n c.setFillColor(GREEN)\n c.drawString(M + 82, y - 68, f\"www.lou-ka.com/logement/{uid}\")\n y -= 86\n c.setFont(\"Helvetica\", 6.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources : Statistique Canada (2021), INSPQ (CC-BY 4.0), \"\n \"Ville de Montréal (CC-BY 4.0), OpenStreetMap, BDZI, RSQAQ, \"\n \"Hydro-Québec, registre-des-loyers.ca, SOQUIJ, gazquebec.ca.\")\n\n s.pied(_PIED_FICHE, page[0])\n c.save()\n return buf.getvalue()\n\n\ndef s_titre_droite(c, y, x, texte):\n c.setFont(\"Helvetica-Bold\", 12.5)\n c.setFillColor(INK)\n c.drawString(x, y, texte)\n c.setStrokeColor(LIME)\n c.setLineWidth(3)\n c.line(x, y - 4, x + c.stringWidth(texte, \"Helvetica-Bold\", 12.5), y - 4)\n return y - 18\n\n\n# ---------------------------------------------------------------------------\n# Rapport global du marché\n# ---------------------------------------------------------------------------\n\n_REGIONS = [\n (\"Québec métro\", {\"Québec\", \"Lévis\", \"Saint-Augustin-de-Desmaures\", \"L'Ancienne-Lorette\", \"Pont-Rouge\", \"Shannon\"}),\n (\"Outaouais\", {\"Gatineau\", \"Chelsea\", \"Thurso\", \"Perkins\", \"Maniwaki\", \"Val-des-Monts\"}),\n (\"Estrie / Montérégie-Est\", {\"Sherbrooke\", \"Magog\", \"Orford\", \"East Angus\", \"Waterville\", \"Granby\", \"Waterloo\", \"Bromont\", \"Cowansville\", \"Richmond\"}),\n (\"Mauricie / Centre-du-Québec\", {\"Trois-Rivières\", \"Bécancour\", \"Shawinigan\", \"Drummondville\", \"Victoriaville\", \"Notre-Dame-du-Bon-Conseil\", \"Saint-Léonard-d'Aston\", \"Louiseville\", \"Nicolet\", \"Wickham\", \"Saint-Narcisse\", \"Saint-Nicéphore\"}),\n (\"Lanaudière / Laurentides\", {\"Joliette\", \"Saint-Jérôme\", \"Berthierville\", \"Saint-Ambroise-de-Kildare\", \"Saint-Gabriel-de-Brandon\", \"Lachute\", \"Brownsburg-Chatham\", \"Saint-Charles-Borromée\", \"Mirabel\", \"Sainte-Agathe-des-Monts\", \"Sainte-Thérèse\", \"Blainville\", \"Charlemagne\", \"Notre-Dame-des-Prairies\"}),\n (\"Bas-Saint-Laurent / Gaspésie\", {\"Rimouski\", \"Rivière-du-Loup\", \"Matane\", \"Saint-Ulric\", \"Amqui\", \"Le Bic\", \"New Richmond\", \"Carleton-sur-Mer\", \"Gaspé\"}),\n (\"Saguenay–Lac-Saint-Jean\", {\"Saguenay\", \"Alma\", \"Chicoutimi\", \"Jonquière\", \"Chambord\", \"La Malbaie\"}),\n (\"Abitibi-Témiscamingue\", {\"Rouyn-Noranda\", \"Val-d'Or\", \"Amos\", \"Malartic\"}),\n (\"Côte-Nord\", {\"Sept-Îles\", \"Port-Cartier\", \"Baie-Comeau\", \"Forestville\"}),\n (\"Chaudière-Appalaches\", {\"Saint-Georges\", \"Sainte-Marie\", \"Thetford Mines\", \"Montmagny\", \"Vallée-Jonction\", \"Scott\", \"Saint-Isidore\", \"La Guadeloupe\", \"Saint-Raphaël\"}),\n]\n\n\ndef _region_de(city: str) -> str:\n for nom, villes in _REGIONS:\n if city in villes:\n return nom\n return \"Grand Montréal & environs\"\n\n\ndef rapport_pdf() -> bytes:\n \"\"\"Rapport global du marché locatif Lou-Ka (multi-pages).\n\n Tous les chiffres viennent de marketstats.compute() — la même source\n que la page Statistiques du site.\n \"\"\"\n from . import marketstats\n st = marketstats.compute()\n reg_file = json.loads((db.DB_PATH.parent / \"sources.json\").read_text(\"utf-8\"))[\"sources\"]\n noms = {s[\"id\"]: s[\"name\"] for s in reg_file}\n\n t = st[\"totals\"]\n total, moy, med, gps = t[\"total\"], t[\"avg\"] or 0, t[\"median\"] or 0, t[\"gps_pct\"]\n n_sources = t[\"sources\"]\n\n buf = io.BytesIO()\n c = rl_canvas.Canvas(buf, pagesize=letter)\n c.setTitle(\"Lou-Ka — Rapport du marché locatif\")\n s = _Style(c)\n\n # ---- page 1 : couverture + grands chiffres + régions\n s.fond()\n s.logo(M, PAGE_H - 130, 42)\n c.setFont(\"Helvetica-Bold\", 24)\n c.setFillColor(INK)\n c.drawString(M, PAGE_H - 185, \"Rapport du marché locatif\")\n c.setFont(\"Courier-Bold\", 9)\n c.setFillColor(INK3)\n c.drawString(M, PAGE_H - 205,\n time.strftime(\"PROVINCE DE QUÉBEC · GÉNÉRÉ LE %Y-%m-%d · WWW.LOU-KA.COM\"))\n\n grands = [(f\"{total:,}\".replace(\",\", NBSP), \"annonces actives\"),\n (str(n_sources), \"gestionnaires connectés\"),\n (_fmt_money(moy), \"loyer moyen\"),\n (_fmt_money(med), \"loyer médian\"),\n (f\"{gps}{NBSP}%\", \"annonces géolocalisées\")]\n tw = (PAGE_W - 2 * M - 4 * 10) / 5\n for i, (val, lab) in enumerate(grands):\n tx = M + i * (tw + 10)\n ty = PAGE_H - 285\n c.setFillColor(INK)\n c.roundRect(tx + 3, ty - 3, tw, 58, 6, stroke=0, fill=1) # ombre décalée\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(1.5)\n c.roundRect(tx, ty, tw, 58, 6, stroke=1, fill=1)\n c.setFont(\"Helvetica-Bold\", 15)\n c.setFillColor(INK)\n c.drawCentredString(tx + tw / 2, ty + 32, val)\n c.setFont(\"Courier\", 6.3)\n c.setFillColor(INK3)\n c.drawCentredString(tx + tw / 2, ty + 14, lab.upper())\n\n y = s.titre_section(PAGE_H - 330, \"Couverture par région\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"RÉGION\", 0), (\"ANNONCES\", 280), (\"SOURCES\", 360),\n (\"LOYER MOYEN\", 430)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.setStrokeColor(INK)\n c.setLineWidth(1)\n c.line(M, y, PAGE_W - M, y)\n y -= 14\n regions = st[\"by_region\"]\n max_n = max((r[\"count\"] for r in regions), default=1)\n for r in regions:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFillColor(LIME)\n c.setStrokeColor(INK)\n c.setLineWidth(0.7)\n bw = 90 * r[\"count\"] / max_n\n c.roundRect(M + 180, y - 1, max(3, bw), 8, 3, stroke=1, fill=1)\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 330, y, f\"{r['count']:,}\".replace(\",\", NBSP))\n c.drawRightString(M + 400, y, str(r[\"sources\"]))\n c.drawRightString(M + 500, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n y -= 15\n s.pied(\"Rapport généré automatiquement à partir des annonces publiques \"\n \"agrégées par Lou-Ka. Loyers : bornes 300–10 000 $.\", 1)\n c.showPage()\n\n # ---- page 2 : histogramme des loyers + par type\n s.fond()\n y = s.entete(\"Rapport du marché · loyers\")\n y = s.titre_section(y, \"Distribution des loyers\")\n hist = st[\"histogram\"]\n max_c = max((h[\"count\"] for h in hist), default=1) or 1\n ch_h, ch_y = 150, y - 170\n bw = (PAGE_W - 2 * M) / len(hist)\n for i, h in enumerate(hist):\n bh = ch_h * h[\"count\"] / max_c\n bx = M + i * bw\n borne = h[\"hi\"] is None or h[\"lo\"] == 0\n c.setFillColor(INK3 if borne else GREEN)\n c.setStrokeColor(INK)\n c.setLineWidth(0.6)\n c.rect(bx + 2, ch_y, bw - 4, max(1, bh), stroke=1, fill=1)\n if h[\"count\"] and h[\"count\"] > max_c * 0.06:\n c.setFont(\"Courier-Bold\", 6)\n c.setFillColor(INK)\n c.drawCentredString(bx + bw / 2, ch_y + bh + 3, str(h[\"count\"]))\n c.setFont(\"Helvetica\", 5.6)\n c.setFillColor(INK3)\n lab = f\"<{h['hi']}\" if h[\"lo\"] == 0 else (f\"{h['lo']}+\" if h[\"hi\"] is None\n else str(h[\"lo\"]))\n c.drawCentredString(bx + bw / 2, ch_y - 9, lab)\n y = ch_y - 30\n\n y = s.titre_section(y, \"Par taille de logement\")\n c.setFont(\"Courier-Bold\", 7)\n c.setFillColor(INK3)\n for lab, xoff in ((\"TAILLE\", 0), (\"ANNONCES\", 160), (\"LOYER MOYEN\", 260),\n (\"LOYER MIN\", 360)):\n c.drawString(M + xoff, y, lab)\n y -= 4\n c.line(M, y, PAGE_W - M, y)\n y -= 13\n for r in st[\"by_type\"][:9]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawRightString(M + 220, y, str(r[\"count\"]))\n c.drawRightString(M + 330, y, _fmt_money(r[\"avg_price\"]) if r[\"avg_price\"] else \"—\")\n c.drawRightString(M + 430, y, _fmt_money(r[\"min_price\"]) if r[\"min_price\"] else \"—\")\n y -= 13\n s.pied(\"Lou-Ka — agrégateur indépendant. Chaque annonce renvoie à la \"\n \"source originale du gestionnaire.\", 2)\n c.showPage()\n\n # ---- page 3 : offre, inclusions, prix au pi², baisses de prix\n s.fond()\n y = s.entete(\"Rapport du marché · offre\")\n o = st[\"offre\"]\n y = s.titre_section(y, \"Inclusions et caractéristiques du parc\")\n carac = [(\"Chauffage inclus\", o[\"chauffage_pct\"]),\n (\"Électricité incluse\", o[\"electricite_pct\"]),\n (\"Eau chaude incluse\", o[\"eau_chaude_pct\"]),\n (\"Internet inclus\", o[\"internet_pct\"]),\n (\"Climatisation\", o[\"clim_pct\"]),\n (\"Stationnement\", o[\"stationnement_pct\"]),\n (\"Balcon\", o[\"balcon_pct\"]),\n (\"Meublé\", o[\"furnished_pct\"])]\n for lab, v in carac:\n if v is None:\n continue\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, lab)\n bx0, bw_ = M + 150, PAGE_W - 2 * M - 200\n c.setFillColor(SURFACE)\n c.setStrokeColor(INK)\n c.setLineWidth(0.8)\n c.roundRect(bx0, y - 1, bw_, 8, 4, stroke=1, fill=1)\n c.setFillColor(GREEN)\n c.roundRect(bx0, y - 1, bw_ * min(1, v / 100), 8, 4, stroke=0, fill=1)\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(PAGE_W - M, y, f\"{v}{NBSP}%\")\n y -= 15\n y -= 6\n c.setFont(\"Helvetica\", 8)\n c.setFillColor(INK3)\n c.drawString(M, y, f\"Disponibles maintenant : {o['dispo_now']:,} · à date future : \"\n f\"{o['dispo_date']:,} · superficie moyenne : \"\n f\"{o['superficie_moyenne'] or '—'} pi² \"\n f\"({o['superficie_connue']:,} annonces la publient)\"\n .replace(\",\", NBSP))\n y -= 24\n\n if o[\"prix_pi2\"]:\n y = s.titre_section(y, \"Prix au pied carré (loyer / superficie)\")\n for r in o[\"prix_pi2\"][:6]:\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.setFillColor(INK)\n c.drawString(M, y, r[\"key\"])\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M + 70, y, f\"{r['val']:.2f}\".replace(\".\", \",\") +\n f\"{NBSP}$/pi² · {r['count']} annonces\")\n y -= 13\n y -= 10\n\n if st[\"baisses\"]:\n y = s.titre_section(y, \"Baisses de prix récentes (30 jours)\")\n for b in st[\"baisses\"][:8]:\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(M, y, f\"{(b['title'] or b['uid'])[:46]} — {b['city'] or ''}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(GREEN)\n c.drawRightString(PAGE_W - M, y,\n f\"{_fmt_money(b['avant'])} → {_fmt_money(b['apres'])} \"\n f\"({b['pct']}{NBSP}%)\".replace(\".\", \",\"))\n y -= 13\n s.pied(\"Caractéristiques dérivées des annonces publiées ; les inclusions \"\n \"non mentionnées par une source ne sont pas comptées.\", 3)\n c.showPage()\n\n # ---- page 4 : top villes + top gestionnaires\n s.fond()\n y = s.entete(\"Rapport du marché · détail\")\n y = s.titre_section(y, \"Top 20 des villes\")\n col_w = (PAGE_W - 2 * M) / 2\n for i, r in enumerate(st[\"by_city\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {r['key']}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 16\n\n y = s.titre_section(y, \"Top 20 des gestionnaires\")\n for i, r in enumerate(st[\"by_source\"][:20]):\n vx = M if i < 10 else M + col_w\n vy = y - (i % 10) * 13\n c.setFont(\"Helvetica\", 8.5)\n c.setFillColor(INK2)\n c.drawString(vx, vy, f\"{i + 1:>2}. {noms.get(r['key'], r['key'])[:34]}\")\n c.setFont(\"Courier-Bold\", 8)\n c.setFillColor(INK)\n c.drawRightString(vx + col_w - 24, vy, f\"{r['count']:,}\".replace(\",\", NBSP))\n y -= 10 * 13 + 20\n\n c.setFont(\"Helvetica\", 7.5)\n c.setFillColor(INK3)\n c.drawString(M, y, \"Sources de données de quartier : Statistique Canada (Recensement 2021, \"\n \"licence ouverte), INSPQ (CC-BY 4.0), Ville de Montréal (CC-BY 4.0), OpenStreetMap.\")\n s.pied(\"© Lou-Ka — www.lou-ka.com · rapport non contractuel, généré automatiquement.\", 4)\n c.save()\n return buf.getvalue()\n","structuredPatch":[{"oldStart":639,"oldLines":7,"newStart":639,"newLines":7,"lines":[" + \" — le total réel peut être plus élevé.\","," PAGE_W - 2 * M, taille=7.5,"," couleur=HexColor(\"#8a5a12\"), interligne=9.5,","- max_lignes=2)","+ max_lignes=3)"," pi2_d = cout.get(\"pi2\")"," if pi2_d and pi2_d.get(\"valeur\") is not None:"," v = f\"{pi2_d['valeur']:.2f}\".replace(\".\", \",\")"]}],"userModified":false,"replaceAll":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"81630e87-5984-46f0-9071-556d6f02e096","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN172Zu4rdCMgJ8x6mA","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS7gMKpwEIERgCKkCgDbn/awGPdd9sz9tRLxHv17RffRxrpEukL2novHbkxm+aJ4kUWFbKZgJYA284xULUYCHZRHR7780XrsJ/yvhnMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGni7PUBrABAhIM5p8XCQ/hf9ElbylzGgzKej1/HEwv1XEjkpIiMKk3bYFhNJ1/mXb3Io980w7tBFE3uVaVk9fMD3EwBnCzBGpn4hDK0Pj+aWF11x/9MSrzARP2ilCp3VEzJ4R7DzysYhBVxab/tGSCojDEk0KzOQBMhX2qxO7dRBs2RE80hWzJFYS6VRruqv9KVi3Z9C3J8CUpNXHuXoijLDeL7zXgjW/m4ibxkg+JCLSdeQsSR7UQe/+ShSXwrRM3SzR5sAeZ07RBrhBFCjwoIej/nOFf7XsL06ksjZy+EFsIxIQvHzf0e4x1kb485lERuLQmmJfmkdmnHYhujm2uGDFvNMz1uIvMSIdTUu+D4R4FV8GJhqck+5IFSryQAbmQDcSEHneyhnruZaEWtbD0/qgzwFmlUqBzuHqsVAjNgT/aILj7dj+FWbGkrBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1025,"cache_read_input_tokens":134005,"cache_creation":{"ephemeral_5m_input_tokens":1025,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"99d49ad9-e64f-40b2-a4b8-68b1bffe6880"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN172Zu4rdCMgJ8x6mA","type":"message","role":"assistant","content":[{"type":"text","text":"PDF terminé. Je rebuild le frontend, je redémarre, et je valide le rendu desktop + mobile en navigateur."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1025,"cache_read_input_tokens":134005,"cache_creation":{"ephemeral_5m_input_tokens":1025,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"85251c2c-561b-4c78-af4f-69bec22b6554"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN172Zu4rdCMgJ8x6mA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012bRoS4iZkRW8Lrg84AbGzQ","name":"TodoWrite","input":{"todos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"in_progress","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1025,"cache_read_input_tokens":134005,"cache_creation":{"ephemeral_5m_input_tokens":1025,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b2a39fce-66e9-4050-a1f6-3da8ed0ff1dc"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012bRoS4iZkRW8Lrg84AbGzQ","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"176e9071-54b7-41d4-8f38-4bdf99c16a61","timestamp":"2026-08-24T22:28:58.832Z","tool_use_result":{"oldTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"in_progress","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"pending","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}],"newTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"in_progress","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN172Zu4rdCMgJ8x6mA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HGWpChdifBXVgRXZmLrvU1","name":"Bash","input":{"command":"cd frontend && npm run build 2>&1 | tail -3 && cd .. && pm2 restart lou-ka-web --update-env 2>&1 | grep -c \"✓\" && sleep 2 && curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8095/api/stats","description":"Build frontend, restart web, healthcheck","timeout":300000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":1025,"cache_read_input_tokens":134005,"cache_creation":{"ephemeral_5m_input_tokens":1025,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d833aea1-cb3c-40c3-bed3-7cbfe382e12b"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bn0jklx0v","tool_use_id":"toolu_01HGWpChdifBXVgRXZmLrvU1","description":"Build frontend, restart web, healthcheck","task_type":"local_bash","uuid":"6d2eb821-c5c7-4e5b-b1bf-03dc8602adc1","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bn0jklx0v","tool_use_id":"toolu_01HGWpChdifBXVgRXZmLrvU1","status":"completed","output_file":"","summary":"Build frontend, restart web, healthcheck","uuid":"c7a37124-f7f8-4f21-88a7-cb7066949bcc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HGWpChdifBXVgRXZmLrvU1","type":"tool_result","content":"- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.\n✓ built in 2.82s\n1\n200","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ab47d8e3-d98b-4074-aae2-352d6eff778a","timestamp":"2026-08-24T22:29:08.071Z","tool_use_result":{"stdout":"- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.\n✓ built in 2.82s\n1\n200","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"cbdb9c73-b100-4d3e-a1be-7863682c98bc","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN2QY4QJuwYaz729evT","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QXy4tU3anoTMckwLAhkecc","name":"Bash","input":{"command":"UID_L=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nprint(con.execute(\\\"SELECT uid FROM listings WHERE active=1 AND source='logisco' LIMIT 1\\\").fetchone()[0])\") && cd frontend && node -e \"\nconst { chromium } = require('playwright');\n(async () => {\n const b = await chromium.launch();\n for (const [name, vp] of [['desktop', {width:1440, height:900}], ['mobile', {width:390, height:844}]]) {\n const p = await b.newPage({ viewport: vp, deviceScaleFactor: 1 });\n await p.goto('http://localhost:8095/logement/' + encodeURIComponent('$UID_L'), { waitUntil: 'networkidle' });\n await p.waitForTimeout(1800);\n for (const sel of ['.f-coutreel', '.f-gest', '.f-hiver', '.f-histolk', '.f-immeuble']) {\n const el = await p.\\$(sel);\n if (el) { await el.screenshot({ path: '/tmp/blk-' + name + sel.replace('.','-') + '.png' }); }\n else console.log(name, sel, 'ABSENT');\n }\n await p.close();\n }\n await b.close();\n console.log('captures OK');\n})();\" 2>&1","description":"Screenshot each new block at 1440px and 390px","timeout":180000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":885,"cache_read_input_tokens":135030,"cache_creation":{"ephemeral_5m_input_tokens":885,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"804d2094-ef5a-4ebd-bcb4-75960d45604e"}} {"type":"claude","event":{"type":"system","subtype":"task_started","task_id":"bia8dxt2r","tool_use_id":"toolu_01QXy4tU3anoTMckwLAhkecc","description":"Screenshot each new block at 1440px and 390px","task_type":"local_bash","uuid":"aac62aec-53a1-4beb-8cfc-4e8dd6d3d705","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"system","subtype":"task_notification","task_id":"bia8dxt2r","tool_use_id":"toolu_01QXy4tU3anoTMckwLAhkecc","status":"completed","output_file":"","summary":"Screenshot each new block at 1440px and 390px","uuid":"68870c3c-7edd-4c88-bfc5-cf493f072513","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QXy4tU3anoTMckwLAhkecc","type":"tool_result","content":"captures OK","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"22d1e6f6-4cb3-4222-96e1-0509ee9bae19","timestamp":"2026-08-24T22:29:25.492Z","tool_use_result":{"stdout":"captures OK","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"8694323d-e74b-41a1-9700-668012a2f19c","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN3gwpCD78iMgcyc4En","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XL5novFcC6LzDfrdn8wZN5","name":"Read","input":{"file_path":"/tmp/blk-mobile-f-coutreel.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":573,"cache_read_input_tokens":135915,"cache_creation":{"ephemeral_5m_input_tokens":573,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ca3491cb-984f-4035-af23-b2f4e8152a5e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XL5novFcC6LzDfrdn8wZN5","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"7e35a0df-dfa6-414b-93f0-f51c47dc8dd2","timestamp":"2026-08-24T22:29:29.043Z","tool_use_result":{"type":"image","file":{"base64":"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","type":"image/png","originalSize":104748,"dimensions":{"originalWidth":390,"originalHeight":586,"displayWidth":390,"displayHeight":586}}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN3gwpCD78iMgcyc4En","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UKMEKn1rRUuxuuuH2EbcqE","name":"Read","input":{"file_path":"/tmp/blk-mobile-f-gest.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":573,"cache_read_input_tokens":135915,"cache_creation":{"ephemeral_5m_input_tokens":573,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ea12993d-9f1c-40d9-a502-381a0e4f6016"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UKMEKn1rRUuxuuuH2EbcqE","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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p11mzpp5zkXnUQaYGpzRSOwg+9Tp0y7tcwXERKBGPbzwDbfefFHvS668rs/l11y5aPFiI+IGHeii3pdOmTa1+DgzVaEMGzH8occfMX5y9B/G+dGnHv918G/5cVZkZtKrS5ctvfWuO04554yTzzod7pVxdcV1fYDtab3OOvWcM/Hv/kceZLyhvP/xR3t0271UqVKonzptWq+Lz7/+lpvOvfTC2+65c/WaNZSN4uO8YOHCex68D66NtnQq2HevvT/67BPnsP6r7DkO52LaM6Usjc31jbPnjcVZDqEIzq5xAT3RwvM55iij+K1YocLAn3/u3rWr7Yv4huBZvnx54y8OXRemP+m/VUyyx4jEhRGJnesVvEdA9cqehdVi9Wtr5s+ff+3NN5x6wkkvvPqy1dbb771btmzZ5598Bg0eefLxV9947dwzz4mTgjJZTJyZXDjo2mW3XXbamV1lXTY1zo/c+2DQfBnOjILF+Ypr+6xctarKFlXgmyTOsKL69ervu+fetrJO7dpGhPGhw4f2uexKe/zMC8923XW3Y488GrknHNMXX315YM/9/wbOGOjVZ1+Em6O+wwX2Vi1aYs79NXFC65at/pM4F9OegzgXx56LebxJ7VmLuwHafqEkSILWyEfgd9phx8+//gIuyRL6+vtvt+ncecjQIY7ugA/fH/jLTymd2mXHnQ8+4EAs/vvceO2l51/coH59XJ0zd+4d99516419y5crN+CD938a9DNyrh7ddodJYSzkyWiDBH7w0KH169Y798yzq1atCrO7tE/vXmec/fpbb2CttN/e++yzx152OATtTz7/dPbcOZ06dDz+6OMqVaxI0UF8Qxe0qVSp0n577+tAmTZ92lsD3v3jrz8bNWx4yvEn1a+X+YAn1gvPvfQCKmvWqHnCMce1adXatcdi4dbrb0bj5195yaK5ZaNGu+60iwWnc8eOH3z8EdMWjr/94ftPv/hs9ZrVRxx8mMN5xYoVr7zx+sjRIytWrHT0YUds0ynzHdH3P/4QrGJhMnHSpIfuuf/ehx/oukuX7378YdKUyZDrjJNPRcex48d98MlHV15yOUih5dvvDbD8n3zciQAWEF129RXHHXnMq2/2P3C/nnv12POrb7/+9IvPscrefpttTzz2eLfewXDjfh//8eefNt5yKzRo37btaSee0u+FZ0ePHdO8WfMLzzmvUvTx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Goqy4NlzZwG4ZOfwkSPbdmyPyJatXt16kQULqjEJ/bVz966oYlH169aTBdW0SP7F0z179+TKlavJVY2leJ8+fXrthvXtWreRvOw/cOB0yuk6tWrLIvv279uxa1eRwoUlv0gEwQUKFChftpykZO6C+Y2valQgf/5NNua5cuXeuHkT8tetXUe9CGriLLze0KXoDbrVATdcJFltBM9yXker0NRw1upW5j91eOfRravy58PvNMZzQ6ekpWbGn8iIOZaZPf58kWieM0f6qTPwn0Qm42fTYhP2roFKUmPmlptvgQaZOm3qu2+/CyiXLF2CoYLBUL9efegRzIwg95Kl4sWLT5k4BY+OHD1yx9137Nq1S1KCkfb7+N/LlyvPbBW5atWqRTOiwpoAABAASURBVIsXycp79+kNlYTxfGPXGw8fPiwb7dSx03cjv8PTV199NelE0tx5c3/97Vek33v3vQUjC3419CsLsoiI8ePGY+ChyKDPBn348YcSIqSP+nbUNVdfA3F/+tmnq1etfv11138/6vtJUyY93+f5gYMHSksCXno80gM3oH/6jOmSzjy58wwfOhyKzxfncb+MgzrLmStn1y5dkfLeB+/NXzD/m6+/Qbuo9qNPPpIcASIQAPWH++49usvKURzBi+FfDb/u2uuoPgLlH3z0gWwCSmfwp4Nvv+12CWbC8YRGzRoBaguQDp2+//Z73ICA+x+6H7DLIn2e6fPKS68AjVE/jJo4eSKIuarhVa++8WrmhczlS5YnJiXe9+B9UEYLuyxUAkZf3qVSx40I6Kw5s7/54bvaNWsdTzyeeubMx2+/D+WI9C+GD337tTelSoo7fGjYtyOgCEwhVPejx41dvnLFe/3fiY6KkikI5fR76w38rV616rRZ06fOmN7/1dc1vwOtdLzu+j1790J5fTViZ5+nerdtZSk+jGEoR5lzyLCvQAlUEqUEBhKPoMg2bYnBT+iIjZs2NqhXX5D9G1H5W6++USB/gVVrV3/25ZD6deufOZM6ctR3H779bqmSVuTx088Hbd2+vVqVqtBu5cuWffWlvuhWqheGjvx62YrlqBYgjx7304D3P4ISORYfP2TYl+3btJU4L1+1Yt/+/VAo+Akncd3G9fVq19l34MDPv49/57X+CG9NmjalRrXqFcqVlz3++dAvBn00IH++fBOnToam3rlrFyKq6zduuKZd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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9f27427a-d99b-444f-b67c-433b2aead747","timestamp":"2026-08-24T22:29:29.227Z","tool_use_result":{"type":"image","file":{"base64":"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p11mzpp5zkXnUQaYGpzRSOwg+9Tp0y7tcwXERKBGPbzwDbfefFHvS668rs/l11y5aPFiI+IGHeii3pdOmTa1+DgzVaEMGzH8occfMX5y9B/G+dGnHv918G/5cVZkZtKrS5ctvfWuO04554yTzzod7pVxdcV1fYDtab3OOvWcM/Hv/kceZLyhvP/xR3t0271UqVKonzptWq+Lz7/+lpvOvfTC2+65c/WaNZSN4uO8YOHCex68D66NtnQq2HevvT/67BPnsP6r7DkO52LaM6Usjc31jbPnjcVZDqEIzq5xAT3RwvM55iij+K1YocLAn3/u3rWr7Yv4huBZvnx54y8OXRemP+m/VUyyx4jEhRGJnesVvEdA9cqehdVi9Wtr5s+ff+3NN5x6wkkvvPqy1dbb771btmzZ5598Bg0eefLxV9947dwzz4mTgjJZTJyZXDjo2mW3XXbamV1lXTY1zo/c+2DQfBnOjILF+Ypr+6xctarKFlXgmyTOsKL69ervu+fetrJO7dpGhPGhw4f2uexKe/zMC8923XW3Y488GrknHNMXX315YM/9/wbOGOjVZ1+Em6O+wwX2Vi1aYs79NXFC65at/pM4F9OegzgXx56LebxJ7VmLuwHafqEkSILWyEfgd9phx8+//gIuyRL6+vtvt+ncecjQIY7ugA/fH/jLTymd2mXHnQ8+4EAs/vvceO2l51/coH59XJ0zd+4d99516419y5crN+CD938a9DNyrh7ddodJYSzkyWiDBH7w0KH169Y798yzq1atCrO7tE/vXmec/fpbb2CttN/e++yzx152OATtTz7/dPbcOZ06dDz+6OMqVaxI0UF8Qxe0qVSp0n577+tAmTZ92lsD3v3jrz8bNWx4yvEn1a+X+YAn1gvPvfQCKmvWqHnCMce1adXatcdi4dbrb0bj5195yaK5ZaNGu+60iwWnc8eOH3z8EdMWjr/94ftPv/hs9ZrVRxx8mMN5xYoVr7zx+sjRIytWrHT0YUds0ynzHdH3P/4QrGJhMnHSpIfuuf/ehx/oukuX7378YdKUyZDrjJNPRcex48d98MlHV15yOUih5dvvDbD8n3zciQAWEF129RXHHXnMq2/2P3C/nnv12POrb7/+9IvPscrefpttTzz2eLfewXDjfh//8eefNt5yKzRo37btaSee0u+FZ0ePHdO8WfMLzzmvUvTx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Goqy4NlzZwG4ZOfwkSPbdmyPyJatXt16kQULqjEJ/bVz966oYlH169aTBdW0SP7F0z179+TKlavJVY2leJ8+fXrthvXtWreRvOw/cOB0yuk6tWrLIvv279uxa1eRwoUlv0gEwQUKFChftpykZO6C+Y2valQgf/5NNua5cuXeuHkT8tetXUe9CGriLLze0KXoDbrVATdcJFltBM9yXker0NRw1upW5j91eOfRravy58PvNMZzQ6ekpWbGn8iIOZaZPf58kWieM0f6qTPwn0Qm42fTYhP2roFKUmPmlptvgQaZOm3qu2+/CyiXLF2CoYLBUL9efegRzIwg95Kl4sWLT5k4BY+OHD1yx9137Nq1S1KCkfb7+N/LlyvPbBW5atWqRTOiwpoAABAASURBVIsXycp79+kNlYTxfGPXGw8fPiwb7dSx03cjv8PTV199NelE0tx5c3/97Vek33v3vQUjC3419CsLsoiI8ePGY+ChyKDPBn348YcSIqSP+nbUNVdfA3F/+tmnq1etfv11138/6vtJUyY93+f5gYMHSksCXno80gM3oH/6jOmSzjy58wwfOhyKzxfncb+MgzrLmStn1y5dkfLeB+/NXzD/m6+/Qbuo9qNPPpIcASIQAPWH++49usvKURzBi+FfDb/u2uuoPgLlH3z0gWwCSmfwp4Nvv+12CWbC8YRGzRoBaguQDp2+//Z73ICA+x+6H7DLIn2e6fPKS68AjVE/jJo4eSKIuarhVa++8WrmhczlS5YnJiXe9+B9UEYLuyxUAkZf3qVSx40I6Kw5s7/54bvaNWsdTzyeeubMx2+/D+WI9C+GD337tTelSoo7fGjYtyOgCEwhVPejx41dvnLFe/3fiY6KkikI5fR76w38rV616rRZ06fOmN7/1dc1vwOtdLzu+j1790J5fTViZ5+nerdtZSk+jGEoR5lzyLCvQAlUEqUEBhKPoMg2bYnBT+iIjZs2NqhXX5D9G1H5W6++USB/gVVrV3/25ZD6deufOZM6ctR3H779bqmSVuTx088Hbd2+vVqVqtBu5cuWffWlvuhWqheGjvx62YrlqBYgjx7304D3P4ISORYfP2TYl+3btJU4L1+1Yt/+/VAo+Akncd3G9fVq19l34MDPv49/57X+CG9NmjalRrXqFcqVlz3++dAvBn00IH++fBOnToam3rlrFyKq6zduuKZd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","type":"image/png","originalSize":123487,"dimensions":{"originalWidth":390,"originalHeight":684,"displayWidth":390,"displayHeight":684}}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"7faed489-db36-4492-8405-3f3cded6dad4","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN3wxTLE35RpPkBzqek","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIStwYKpwEIERgCKkAXw1pzIywSCJGoXEknRClFyYXxb1YSpCNO78xDduoxOnWgkTXMhjV+svVuPsS0Nft7oVErPRwTy5OXNx3mOWxXMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHQi7PUBrABAhIMhEpsHtJrmMC59vWmGgw4KkxMIUA+ldKDGoYiMDb6UjuXY4LA2dYF0Nhpyky0BcSha65OpiJAaF7KoWcVqZYaPHmIsaboKkXkAZHdxCq8BN2rYPXnXFT9ECzAIMawkL5tOLIt8hlPpkYeSOsrvq5aUaxdLCKH99rxCQ5kmpQ1ZbI7z+gdPaJxfELohf20yYND2U5fAIhkvUmxLFepw5Wr3yPvZ0J1SOSNbBI9dpWrDMvNyuJcAwaP0IiITjGYvrgKoZZfoZEpcgUnCTpq81XLXepPTkO2wQSLA/tfmlPHF4ZJhaTvE4MCvAJHMYVmaZd/YTW3JhbAE+c1Uq7UaD5HxauyxvuzaCpjeBiK/hRKm5JS4XrL3l+COqsRUaed1yrgts3QDZSwt65FyR3nL4asSgm+wXBJsW23+tanFofvv20cONaVX44Rr2SE4EWC5/E19TrwLY6hloOy4GGHiYBSQed986qXcukHe2rVEERSdYD73U26oY80njJNP9HiW8DGsuYbJcltax6G4fsjBaOz4fEWfHf+zbh4wxzBXTSvSNcNSrDrMiouqo8wquI0YV7HF4Gc5/rpd9wJVQi8LrWWfJz+vpz6kXz4U4e0x2xqtoUW1cWEB8Z71TOFnSO1fk6OJIvlKXuYq0mQ2tXs2cI+E7cTQdM0NsF4ak/aPdw0U+MUJ8I5YeASx9SIhDU6lUWKwyAE8tRVVEr1TP6OOow1hvyfnvGi3dSwlrxKwjCAdRTixMeq7FhU5DKc636rurTfYRybTFtNii57BTh7QHj8t1pHP1aEPqgKKmNSZ3XFWeF42v+3gAEvI3ar5kuuLtxIVKM2F/jbsMu47AYWCne0imo6W6OhlAjOwE0MGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":839,"cache_read_input_tokens":136488,"cache_creation":{"ephemeral_5m_input_tokens":839,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b99dbe73-5df4-40cc-b1f6-3a51ca63e353"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN3wxTLE35RpPkBzqek","type":"message","role":"assistant","content":[{"type":"text","text":"Mobile impeccable. Je vérifie le desktop (hiver + historique)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":839,"cache_read_input_tokens":136488,"cache_creation":{"ephemeral_5m_input_tokens":839,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"84cb83ab-ab39-465e-801e-30a2acea08bd"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN3wxTLE35RpPkBzqek","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DztcVXi3MzwjE5iFymGi18","name":"Read","input":{"file_path":"/tmp/blk-desktop-f-hiver.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":839,"cache_read_input_tokens":136488,"cache_creation":{"ephemeral_5m_input_tokens":839,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"59f69840-1f74-48a8-a2c9-e3c7386fee1b"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DztcVXi3MzwjE5iFymGi18","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"27f4bd28-a5e7-4fff-8f15-94fd94a40e03","timestamp":"2026-08-24T22:29:36.808Z","tool_use_result":{"type":"image","file":{"base64":"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","type":"image/png","originalSize":85011,"dimensions":{"originalWidth":432,"originalHeight":429,"displayWidth":432,"displayHeight":429}}}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN3wxTLE35RpPkBzqek","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XCD3L8J5etgjXbY3Dkt7rf","name":"Read","input":{"file_path":"/tmp/blk-desktop-f-histolk.png"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":839,"cache_read_input_tokens":136488,"cache_creation":{"ephemeral_5m_input_tokens":839,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2d5e181e-aafb-4ea8-bd23-3dcbfcf01b7e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XCD3L8J5etgjXbY3Dkt7rf","type":"tool_result","content":[{"type":"image","source":{"type":"base64","data":"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","media_type":"image/png"}}]}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"836cc315-2fc4-4c86-a62e-7dfeac1566ff","timestamp":"2026-08-24T22:29:37.141Z","tool_use_result":{"type":"image","file":{"base64":"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","type":"image/png","originalSize":67325,"dimensions":{"originalWidth":432,"originalHeight":397,"displayWidth":432,"displayHeight":397}}}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"050d3766-aa11-4b0e-9982-bcffd1fb050f","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN4Y34N34PDVhDoKg4U","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS3wQKpwEIERgCKkCYj8G5g8WYIuHtNLeJteGe/sOb6HofHX8eyTpCjqdraW4DB9i3A86/yPmzAy4KwPxp/B0/bRnWfk8J8s8lpwTIMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHXi7PUBrABAhIMliQauji2CWHkXUDdGgw0lGym3VJKGCLJocAiMFQMcUgihEaIqyacuO4CwcnvzjsO+hJVWvAZ9nGDOEkfMoG7p39HhShif8cjnY2VcirkArSJUgKKmqAMF7nH3L/EZYCzFJ0iU3hRSkcGJqKAvBUBzsSbRcsNYXFBcbnY5cNf4UJeOISVHDB5NWTgZw/FsyZ53rSXCJ1FqCfGDTy0O440r06aV29+pM+Jsb5aZ/2ILRODckUtiT5ALTl1FoLJTfcrZ1aajEZNPNwDKDWtDZ4meSW1AxTk04bUAyf9aLz8WNqxxDegD7fK5jB2mOZe+Y1OMIOd9/w0qF+baG5UQonEQkQbn9TP7lEco9ild+av/xzB5m2pavChbOdpZh2VFdippa/NHe/RhVW7XkrYt6vYxKNQvg7cvNNHxE6j1f1mjfui++V9B2/qRnG4tFBDVkGyGGuHJq87EuMJ2EPRNU86D8qjvETdNNa+NNrnt1NLoE0T2sD9Il1pwJFxM8bjarV0npDtU4plNTXBCcTID9VWXL36YFnDK0fhsRttddql6ybD9SFaO0oPx/XbmVjO05ZqsxInGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":920,"cache_read_input_tokens":137327,"cache_creation":{"ephemeral_5m_input_tokens":920,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"002c2488-cb81-4d26-8989-bd417326f545"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN4Y34N34PDVhDoKg4U","type":"message","role":"assistant","content":[{"type":"text","text":"Rendu desktop nickel. Dernière vérification du PDF via l'endpoint live, puis commit."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":920,"cache_read_input_tokens":137327,"cache_creation":{"ephemeral_5m_input_tokens":920,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"532deef6-f3c3-48a6-af09-c8ea36f79b34"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN4Y34N34PDVhDoKg4U","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DZXd5o7AhYX5zcnk4qnpfd","name":"Bash","input":{"command":"cd /Users/simon-pierreboucher/apps/lou-ka && grep -n \"pdf\" louka/web.py | head -4; UID_L=$(.venv/bin/python -c \"\nfrom louka import db\ncon = db.connect()\nprint(con.execute(\\\"SELECT uid FROM listings WHERE active=1 AND source='logisco' LIMIT 1\\\").fetchone()[0])\") && curl -s -o /tmp/fiche-live.pdf -w \"%{http_code} %{size_download}o\\n\" \"http://localhost:8095/api/listings/$UID_L/pdf\" && pdftotext /tmp/fiche-live.pdf - | grep -c \"Coût réel mensuel\\|Qui gère ce logement\\|Vie quotidienne en hiver\\|Passeport de l'immeuble\\|Historique Lou-Ka\"; pdftotext /tmp/fiche-live.pdf - | grep \"lecture humaine\"","description":"Test live PDF endpoint and verify sections","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":920,"cache_read_input_tokens":137327,"cache_creation":{"ephemeral_5m_input_tokens":920,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"20ea1d16-c6ad-41a6-82bf-d9a93ca530c9"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DZXd5o7AhYX5zcnk4qnpfd","type":"tool_result","content":"511:@app.get(\"/api/listings/{uid}/pdf\")\n512:def listing_pdf(uid: str):\n513: \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n515: from . import pdfgen\n200 463894o\n5","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d3bcb73f-ef50-4f06-869b-9c0dd3d89008","timestamp":"2026-08-24T22:29:47.773Z","tool_use_result":{"stdout":"511:@app.get(\"/api/listings/{uid}/pdf\")\n512:def listing_pdf(uid: str):\n513: \"\"\"Fiche de propriété PDF (photos, prix, quartier, QR) — voir pdfgen.py.\"\"\"\n515: from . import pdfgen\n200 463894o\n5","stderr":"","interrupted":false,"isImage":false,"returnCodeInterpretation":"No matches found","noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f3e937ca-ea2e-4ff2-95e0-ce2bde98bb30","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN5KSYxV7yYC7CKm7Ng","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS7wUKpwEIERgCKkA7Xl1TR06bK7qHfrajU7Q79Ys53MbfH6umv0Db48t+SmU4Cx5zjc4pqKJeDqHqpcWgnrGJwm2jMRd2OoOdXP/0Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHhi7PUBrABAhIMPSOQ6tc8LhRiHTlbGgzzy2Pf9iw6+/vNzxgiMAHlAIdndyyb+SnroTUQWBk06l7419YaGP9ufUAloWtkjakcXCyf1zXNhqnmUh+9bCr0A2EWLRUEcI4N1NU0ZpAfohgf5JBI5kVwl+BUuUVN2UHSI45MhUeVAmJ8Ci7b6lAL/z1LA7SQLIq4bhq2CmI8O6EsGw657TGUt/QuMZq2ELF6ZRa9ncBvxYnWYN3I8+92XyGVKnfG9kPOYPtYnFUfqpvdZUHJen1f7pU3h5qQyESd62ofJRqYWQREjWyUnLZAU10pZi1FFu3YO5LcPQGd3SrAa7ZYd9tn2GRzPY7c3YKM+ocLbAjt+DbbpRDzU9IgOiipyI6znnFK4GLPpFyE4lgGoG400sWJU7PXtDIMzM45Tv3W1tWXGYPwwfmQEWLoveivqvzeU/+jFxZPE5yed/hvbkfs8al28q/8UaMPkAkTWudLoJCQEnA+jcWlLQLQddbbCalqLkpzsUilBaouZjenLNhEDvA0Xf5GfvQPn7Zv5me3F6L03f29bNN3zk72uuFyWbVm42TjAB6jGDwGUMU8FDwKnN9aoArroR38mq46Cqr79/Wvx2AnW/UsBeaf73Y18zVWEy2bICAI6pt3mlPXlAYsPssSTRWfVGGNzrKTk+np0StGNY9Ltv0hTysUq+jLfCZCGTar6g0ueWH6nsRVy3QusdzHPW1ePi4aEhaBK8k0+RtFkND7wxL7BZrrnGU1gvf/sdeoZ028IzUjG7XXX5bMGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":695,"cache_read_input_tokens":138247,"cache_creation":{"ephemeral_5m_input_tokens":695,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"6adf4f42-3555-4af4-bc92-dca2571e46a6"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN5KSYxV7yYC7CKm7Ng","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011jP4v7jNtdXtfa7miA46Pg","name":"Bash","input":{"command":"pdftotext /tmp/fiche-live.pdf - | grep -n -A1 \"Fiche Google Maps\"","description":"Verify fine note has no stray ellipsis"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":695,"cache_read_input_tokens":138247,"cache_creation":{"ephemeral_5m_input_tokens":695,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2dad21f4-1812-4760-9669-3bb3910efd1e"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011jP4v7jNtdXtfa7miA46Pg","type":"tool_result","content":"155:Fiche Google Maps associée automatiquement (confiance 100 %). Statistiques sur les 8 avis synchronisés dans la base Lou-Ka — échantillon des plus récents, pas une lecture\n156-humaine.","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"dca28828-019a-4603-812e-3fe52140925a","timestamp":"2026-08-24T22:29:54.319Z","tool_use_result":{"stdout":"155:Fiche Google Maps associée automatiquement (confiance 100 %). Statistiques sur les 8 avis synchronisés dans la base Lou-Ka — échantillon des plus récents, pas une lecture\n156-humaine.","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c09f727e-c94e-486c-8d85-88b9015c4841","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN5oRYt6bTg5QR3aULU","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIShgMKpwEIERgCKkDKFRyOKcOG/8diAI/zI90yxb9YcAwuGMiwLVqULHp8hHzlJmbYJFGJ5xFx9yOccDndMSqQzOxdfEeVz1nSzqI/Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAHni7PUBrABAhIMrfnUTvBLLrp1IcbCGgz/fObRydG/NccMzMQiMGtn5ThgQYvDhzRD1ZZoOScoVEgfL1L97kbWH2jAthhoWY9VVN3N4sNGBTQG0ZQKfiqLAZgcFIoIQTkycJn6ea2XA7/1w4BN9Z5FSt9IKKOD6xJPkEkFZk7Is+VX5ot8mkPQ7SIcfq3VvFK0vWihAevejKEsJJnhacqfocDnJIIK9Mlx3W4FRFvnv+da/1LZzYl0W3dErtJuW0YhE9Tce1bgaJaJ1LhjW85P8emkm8IF33gC18oYTp2JNVHHnFoYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":693,"cache_read_input_tokens":138942,"cache_creation":{"ephemeral_5m_input_tokens":693,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"2dd0814a-68ae-4a7a-834c-ada2ffc4088a"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN5oRYt6bTg5QR3aULU","type":"message","role":"assistant","content":[{"type":"text","text":"Tout est validé (endpoint PDF live 200, 5 sections, texte complet sans ellipse parasite). Je committe styles.css et pdfgen.py — kangalou.py reste à la session ka2."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":693,"cache_read_input_tokens":138942,"cache_creation":{"ephemeral_5m_input_tokens":693,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b4aed392-af0d-4ca5-adbf-04fab5bb5e3b"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN5oRYt6bTg5QR3aULU","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Vari4zT9mgqWnEz3G9TvZm","name":"TodoWrite","input":{"todos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"completed","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"in_progress","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":693,"cache_read_input_tokens":138942,"cache_creation":{"ephemeral_5m_input_tokens":693,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0a599996-6959-455e-a27e-1f3d046e6b3d"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Vari4zT9mgqWnEz3G9TvZm","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"a0c71b71-e706-46ae-8c1d-10ec16ba026e","timestamp":"2026-08-24T22:30:02.719Z","tool_use_result":{"oldTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"in_progress","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"pending","activeForm":"Commit et push"}],"newTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"completed","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"in_progress","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN5oRYt6bTg5QR3aULU","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QCcv2wF1Lm82GP8Zen6Zkf","name":"Bash","input":{"command":"git status --porcelain && git diff -- frontend/src/styles.css louka/pdfgen.py | grep -c cede959","description":"Check status and scan diff for key"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":693,"cache_read_input_tokens":138942,"cache_creation":{"ephemeral_5m_input_tokens":693,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"d5723289-383f-4038-a452-a62bafab0a71"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QCcv2wF1Lm82GP8Zen6Zkf","type":"tool_result","content":" M frontend/src/styles.css\n M louka/pdfgen.py\n0","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"83fab8f4-2cab-4102-b24c-1cebbd01d431","timestamp":"2026-08-24T22:30:03.182Z","tool_use_result":{"stdout":" M frontend/src/styles.css\n M louka/pdfgen.py\n0","stderr":"","interrupted":false,"isImage":false,"returnCodeInterpretation":"No matches found","noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"c3bfe350-a9ad-4af5-8e5d-51ad2119b6fe","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN6UdUoXm1gHvAcptWF","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TKE66SASVpXE1JYUCPJoQ1","name":"Bash","input":{"command":"git add frontend/src/styles.css louka/pdfgen.py && git commit -m \"$(cat <<'EOF'\nfeat(fiche): dossier d'intelligence locative — passe sharp desktop/mobile + PDF complet\n\nCSS : pastilles de statut affûtées, score hiver et note Google en tuiles\nà ombre dure, timeline encre, media queries 640px (tableau coût réel,\ndates empilées, barres d'avis compactes). PDF fiche : cinq nouvelles\nsections fidèles au web — coût réel ligne par ligne (statuts étiquetés),\nhistorique Lou-Ka + vies antérieures, passeport de l'immeuble (tuiles\nKPI, rotation, pression), gestionnaire (note Google, distribution des\navis, plaintes, confiance d'association), score hiver.\n\nCo-Authored-By: Claude Opus 4.7 \nEOF\n)\" && git push origin main && git status --porcelain","description":"Commit CSS + PDF changes and push to spbgit"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":699,"cache_read_input_tokens":139635,"cache_creation":{"ephemeral_5m_input_tokens":699,"ephemeral_1h_input_tokens":0},"output_tokens":17,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"78e23faa-bfde-4461-95bb-6e2c6d8cc26c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"Exit code 128\nfatal: pathspec 'frontend/src/styles.css' did not match any files","is_error":true,"tool_use_id":"toolu_01TKE66SASVpXE1JYUCPJoQ1"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ac1c2d8a-e2b7-498c-ab25-45c90a493171","timestamp":"2026-08-24T22:30:14.125Z","tool_use_result":"Error: Exit code 128\nfatal: pathspec 'frontend/src/styles.css' did not match any files"}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"f87fdff2-8b0d-4abf-8af9-97cb43151b54","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN7HV32xSY49UZwgHaj","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISzAcKpwEIERgCKkA9uN1Fhu6WnxK1n1/kMojq8SSW4kE8U9Fc/9gbIvd0krkOz7Ydv/T6edudXsNLB+xs9VnjYMOhDk2jI7y592fAMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAH+i7PUBrABAhIM401LNt4iiKD8uJPmGgz2M0kmHsai2CqoWNYiMNmqd7QmU76JYQyJvFsh/ReyC2sycH42RbpUXVgbtDmnfNq5gRS3YWaWEA3IiyrlUSrRBTpQJsxOhaRxeeznY2h1/GKxsg60SSZekxea+5+hzghRfKKwYKo33mFChrNKBhJyDcdWCaYTyLlXYHtCOAH5I/5wb3uFcEqvLcL4xG7r3Oze7TtV9/d7fK13ByFGUpwBInoakthk8erk2ef3M4RAKm/ZiAGQ2CniVVJoa7Khh8aidUypib1wx2fX0bxobPSNczHhU4wUQkj3DfCbWUfizBpIUyOuy9pri1Eg6xnoO0TTd8i7hhXETPQSwAsLnxJouGfMfuenzALwJ4wd/DwpykkBxdtv8JDTLU7/jybF7JxiiwlAJm3Qqbhob/pAi1idvIQABb2xflMjVRpcN9qZJas3J16/bzauCApgjeuYjwUYv7+XS+VBOrYJ45QM9081UrSqPm/U5RfSlhKnu8/XrerWBMo6vJfC1MlnkMdzV+8QR4e2xi5nwvWtoRwgZiptacTuUex3cxy/i7vpxdxHGKvc+KVPzwm5BTTIi7ipwaANsCC8CyE1rEV9R7e3YWfI+aUcMCwXeDI+sNpp5ysSAOfVrlMg1O869OsKA2EN2c6juIGJVkHeTGoRGz9R1wor1ajhk1v1Wsa7rh7ysQZbJX+QGJrT2BuFv3ZBT11A6N7DkZKK1x4Ioew/xW54D7u+/AbQBIY+fKV6yizgxN/08CFGSQL5gKtVX3YoblzOFg9xlVkJHWrnoofQPw8TkpK/0VnlCu4PsYLbcNnWe7mSprW0+m/ntPLRNXe+Aqu0f+hXcKIgg0fn8TOdr3bP3vI9NJ0FL0JcPg1AbVyrNriFI5fYMXO81dguiB/+HG2PC491xADEColOfua8f942TPgXhRpjq5x4ld6PIGHQTNKHCvgjVf6bQDGZonj0QwW9ea0T/MiSehgQ5izCHtyAm288jbwtZdddOMwubXLXXJxoRXMwggQJpg+5FsxMJK5MhqP2FtA3R2d/KpQXL3Zre4a5JgoYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":464,"cache_read_input_tokens":140334,"cache_creation":{"ephemeral_5m_input_tokens":464,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"30694d21-57f6-46e0-87bf-9a031a10ef47"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN7HV32xSY49UZwgHaj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015fpiG4RTCpq1MeM5Hydn7A","name":"Bash","input":{"command":"pwd; git rev-parse --show-toplevel; git status --porcelain","description":"Check working directory and repo state"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":464,"cache_read_input_tokens":140334,"cache_creation":{"ephemeral_5m_input_tokens":464,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8d1911d7-f679-4610-9d85-5154eab8c411"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015fpiG4RTCpq1MeM5Hydn7A","type":"tool_result","content":"/Users/simon-pierreboucher/apps/lou-ka/frontend\n/Users/simon-pierreboucher/apps/lou-ka\n M frontend/src/styles.css\n M louka/pdfgen.py","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"46e5cf1c-af6f-4a5a-90da-4ae887995bd1","timestamp":"2026-08-24T22:30:22.696Z","tool_use_result":{"stdout":"/Users/simon-pierreboucher/apps/lou-ka/frontend\n/Users/simon-pierreboucher/apps/lou-ka\n M frontend/src/styles.css\n M louka/pdfgen.py","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"4ff1416f-2763-441c-a381-a73ed8be4bbb","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN7u6fX8Tv8PyJdTpDa","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS2AIKpwEIERgCKkCTBq9xM6QiqRiGIBNeiZ01GhuPoP8Vhj0ZXIlJltgGIo7wX1GQOYFYl2OnrvBsM5a3bbE6aasr4DpZBCms3zOgMg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chCLToDU25FEmjjVqZecraU5iAEBqAGCjLPUBrABAhIMHQBS6h/EbvEK3EeXGgzoc9/3W4hVkR1xXvIiMO2b8ZSzcF9ybduacv2KLhgSZBzI8Sa2NrR+07tuuvQ548Jw6SAI3yvK35cJCYWeIipe5tQ47AFmyDf4ejj3vIUWccI8oT3RL7hY1zQo0JNxyRuW7lPdCVaJYnnLWaYxbF9vHvjSzSvgt8ehIw9LfiL/2moeaMD3CXgfBab89fBsn2E1oyAcL/jVFujuJH3TSBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":432,"cache_read_input_tokens":140798,"cache_creation":{"ephemeral_5m_input_tokens":432,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"b1da07c7-2b0d-4bdd-9bd6-fb8bc94b94b4"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN7u6fX8Tv8PyJdTpDa","type":"message","role":"assistant","content":[{"type":"text","text":"Le répertoire courant avait dérivé dans `frontend/`. Je recommence depuis la racine."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":432,"cache_read_input_tokens":140798,"cache_creation":{"ephemeral_5m_input_tokens":432,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"ca536356-ce02-4c1a-b2e6-c2af6381f989"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN7u6fX8Tv8PyJdTpDa","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01SoHmvwT7h2aQmZi5jXkAS3","name":"Bash","input":{"command":"cd /Users/simon-pierreboucher/apps/lou-ka && git add frontend/src/styles.css louka/pdfgen.py && git commit -m \"$(cat <<'EOF'\nfeat(fiche): dossier d'intelligence locative — passe sharp desktop/mobile + PDF complet\n\nCSS : pastilles de statut affûtées, score hiver et note Google en tuiles\nà ombre dure, timeline encre, media queries 640px (tableau coût réel,\ndates empilées, barres d'avis compactes). PDF fiche : cinq nouvelles\nsections fidèles au web — coût réel ligne par ligne (statuts étiquetés),\nhistorique Lou-Ka + vies antérieures, passeport de l'immeuble (tuiles\nKPI, rotation, pression), gestionnaire (note Google, distribution des\navis, plaintes, confiance d'association), score hiver.\n\nCo-Authored-By: Claude Opus 4.7 \nEOF\n)\" && git push origin main && git status --porcelain","description":"Commit and push from repo root"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":432,"cache_read_input_tokens":140798,"cache_creation":{"ephemeral_5m_input_tokens":432,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"32bb697e-f94c-4f98-a588-b9ba23d46d9c"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01SoHmvwT7h2aQmZi5jXkAS3","type":"tool_result","content":"[main 29b06e9] feat(fiche): dossier d'intelligence locative — passe sharp desktop/mobile + PDF complet\n 2 files changed, 375 insertions(+), 9 deletions(-)\nTo gitsrv:srv/git/lou-ka.git\n a58cabd..29b06e9 main -> main","is_error":false}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"9a38e58f-26fd-4436-b05f-7cab24c72a29","timestamp":"2026-08-24T22:30:31.286Z","tool_use_result":{"stdout":"[main 29b06e9] feat(fiche): dossier d'intelligence locative — passe sharp desktop/mobile + PDF complet\n 2 files changed, 375 insertions(+), 9 deletions(-)\nTo gitsrv:srv/git/lou-ka.git\n a58cabd..29b06e9 main -> main","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"6e7b42a3-7e18-4763-a383-89524fc60e5d","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN8Xae15VZwtLsm6D9N","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PHrmVVafvon7A3X3SMk9dk","name":"TodoWrite","input":{"todos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"completed","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"completed","activeForm":"Commit et push"}]},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":617,"cache_read_input_tokens":141230,"cache_creation":{"ephemeral_5m_input_tokens":617,"ephemeral_1h_input_tokens":0},"output_tokens":25,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"0cb37dc8-5a22-4052-8b59-d2edb692eee2"}} {"type":"claude","event":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PHrmVVafvon7A3X3SMk9dk","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"1da2c96a-ba72-4374-a47e-299c35b64307","timestamp":"2026-08-24T22:30:40.736Z","tool_use_result":{"oldTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"completed","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"in_progress","activeForm":"Commit et push"}],"newTodos":[{"content":"Polir le CSS desktop + mobile des 5 nouveaux blocs (sharp)","status":"completed","activeForm":"Polissage CSS desktop/mobile des nouveaux blocs"},{"content":"Ajouter au PDF fiche : coût réel, historique Lou-Ka, immeuble, gestionnaire, hiver","status":"completed","activeForm":"Ajout des sections dossier au PDF fiche"},{"content":"Build + restart + validation visuelle (Playwright 1440/390) + test PDF","status":"completed","activeForm":"Build, restart et validation"},{"content":"Commit + push origin main (sans clé)","status":"completed","activeForm":"Commit et push"}],"verificationNudgeNeeded":false}}} {"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"fd5fe962-2365-43ee-b322-025668e6f746","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233"}} {"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeNN9Et56rjy5kqMwqgRE","type":"message","role":"assistant","content":[{"type":"text","text":"C'est fait mon ami — tout est sharp, déployé et poussé (`29b06e9`).\n\n**Rendu web (desktop + mobile)**\n- Pastilles de statut (`observé`/`estimé`/`inconnu`) affûtées : bordure 1,5 px, coins 3 px, façon zi-badge\n- Score hiver et note Google ★ en **tuiles à ombre dure** (signature éditorial sharp), timeline à filet encre, survol accent sur le tableau du coût réel, ligne « Total estimé » sur fond accent\n- Media queries 640 px : dates de timeline empilées, tableau compacté, barres d'avis resserrées, détails d'indicateurs sur leur propre ligne — vérifié en navigateur à 1440 px et 390 px sur une annonce Logisco\n\n**PDF de la fiche** — les 5 sections du dossier y sont maintenant, fidèles au web et au principe d'honnêteté :\n- **Coût réel mensuel** : ligne par ligne avec étiquette de statut, total ≈/mois + /an, postes non chiffrables en avertissement, $/pi² avec percentile réel\n- **Historique Lou-Ka** : suivie depuis, jours en ligne, événements datés, vies antérieures probables avec % de confiance et signaux\n- **Passeport de l'immeuble** : 4 tuiles KPI, médianes par chambres, rotation et pression (ou « données insuffisantes »)\n- **Qui gère ce logement?** : note Google en pilule, barres de distribution 5→1★, moyenne 12 mois, plaintes récurrentes, note de confiance d'association\n- **Vie quotidienne en hiver** : score + classe + critères minutés\n\nEndpoint `/api/listings/{uid}/pdf` testé live (200, 5 sections présentes), build + `pm2 restart lou-ka-web` faits, healthcheck OK, aucune clé dans le diff."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":378,"cache_read_input_tokens":141847,"cache_creation":{"ephemeral_5m_input_tokens":378,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","uuid":"8c6605df-0a6d-4e2e-84b9-3a2ed780cbd9"}} {"type":"claude","event":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":501038,"duration_api_ms":477353,"num_turns":53,"result":"C'est fait mon ami — tout est sharp, déployé et poussé (`29b06e9`).\n\n**Rendu web (desktop + mobile)**\n- Pastilles de statut (`observé`/`estimé`/`inconnu`) affûtées : bordure 1,5 px, coins 3 px, façon zi-badge\n- Score hiver et note Google ★ en **tuiles à ombre dure** (signature éditorial sharp), timeline à filet encre, survol accent sur le tableau du coût réel, ligne « Total estimé » sur fond accent\n- Media queries 640 px : dates de timeline empilées, tableau compacté, barres d'avis resserrées, détails d'indicateurs sur leur propre ligne — vérifié en navigateur à 1440 px et 390 px sur une annonce Logisco\n\n**PDF de la fiche** — les 5 sections du dossier y sont maintenant, fidèles au web et au principe d'honnêteté :\n- **Coût réel mensuel** : ligne par ligne avec étiquette de statut, total ≈/mois + /an, postes non chiffrables en avertissement, $/pi² avec percentile réel\n- **Historique Lou-Ka** : suivie depuis, jours en ligne, événements datés, vies antérieures probables avec % de confiance et signaux\n- **Passeport de l'immeuble** : 4 tuiles KPI, médianes par chambres, rotation et pression (ou « données insuffisantes »)\n- **Qui gère ce logement?** : note Google en pilule, barres de distribution 5→1★, moyenne 12 mois, plaintes récurrentes, note de confiance d'association\n- **Vie quotidienne en hiver** : score + classe + critères minutés\n\nEndpoint `/api/listings/{uid}/pdf` testé live (200, 5 sections présentes), build + `pm2 restart lou-ka-web` faits, healthcheck OK, aucune clé dans le diff.","stop_reason":"end_turn","session_id":"62f13d0b-8c21-4252-973c-9bb84aed0233","total_cost_usd":2.2315818000000007,"usage":{"input_tokens":72,"cache_creation_input_tokens":126004,"cache_read_input_tokens":4306036,"output_tokens":31136,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":126004},"inference_geo":"","iterations":[{"input_tokens":2,"output_tokens":666,"cache_read_input_tokens":141847,"cache_creation_input_tokens":378,"cache_creation":{"ephemeral_5m_input_tokens":378,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-fable-5":{"inputTokens":72,"outputTokens":31136,"cacheReadInputTokens":4306036,"cacheCreationInputTokens":126004,"webSearchRequests":0,"costUSD":2.2315818000000007,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"512d5dd8-c494-4c1a-aad4-81a2d547ecb9"}} {"type":"status","state":"idle"}