SPB Git forge

spb/uqo-eval

Public
55commits 2branches 0releases
134.4 MBsize
maindefault branch
1 h agolast push
TypeScript 90.1% JavaScript 3.2% Python 3.2% CSS 1.8% HTML 1.8%

UQO Éval : refonte complète depuis Vrai-Prix — identité UQO officielle (logo PMS 7469), mode étudiant, PDF avec logo UQO + cours IMM1003/IMM1033, retrait SSO/favoris/lead/écosystème KA, assistant Éva local, domaine uqo-eval.app

Simon-Pierre Boucher committed 1 mo ago (Aug 28, 2026) parent cb53ef4

140 changed files +1,636 −3,150

modified README.md +41 −265
@@ -1,140 +1,49 @@
1 1 <p align="center">
2 − <a href="https://www.vrai-prix.com"><img src="https://www.vrai-prix.com/og.png" width="760" alt="Vrai-Prix — La valeur réelle de chaque propriété"></a>
2 + <a href="https://www.uqo-eval.app"><img src="public/og.png" width="760" alt="UQO Éval — Laboratoire pédagogique d'évaluation immobilière"></a>
3 3 </p>
4 4
5 −<h1 align="center">Vrai-Prix</h1>
5 +<h1 align="center">UQO Éval</h1>
6 6
7 −<p align="center"><b>La valeur réelle de chaque propriété</b></p>
7 +<p align="center"><b>Laboratoire pédagogique d'évaluation immobilière</b><br>
8 +<sub>Cours IMM1003 — Éléments d'évaluation immobilière · IMM1033 — Méthodes du coût en évaluation immobilière<br>
9 +enseignés par Simon-Pierre Boucher — Université du Québec en Outaouais</sub></p>
8 10
9 11 <div align="center">
10 12
11 −[![Site](https://img.shields.io/website?url=https%3A%2F%2Fwww.vrai-prix.com&style=flat-square&label=www.vrai-prix.com&up_color=ff5148)](https://www.vrai-prix.com)
12 −[![Documentation](https://img.shields.io/badge/📖_documentation-%2Fdoc-ff5148?style=flat-square)](https://www.vrai-prix.com/doc/index.html)
13 −[![PDF](https://img.shields.io/badge/guide-PDF-ff5148?style=flat-square)](https://www.vrai-prix.com/doc/vrai-prix-documentation.pdf)
14 −![Nœud](https://img.shields.io/badge/n%C5%93ud-M3U96a-1f6feb?style=flat-square)
15 −![Port](https://img.shields.io/badge/port-8090-ff5148?style=flat-square)
16 −![PM2](https://img.shields.io/badge/process-PM2-2b037a?style=flat-square)
17 −
18 −[![Propriétés](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.vrai-prix.com%2Fapi%2Fstats&query=%24.units_total&label=propri%C3%A9t%C3%A9s&color=ff5148&style=flat-square)](https://www.vrai-prix.com/api/stats)
19 −[![Transactions](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.vrai-prix.com%2Fapi%2Fstats&query=%24.transactions&label=transactions%20r%C3%A9elles&color=ff5148&style=flat-square)](https://www.vrai-prix.com/api/stats)
20 −[![Municipalités](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.vrai-prix.com%2Fapi%2Fstats&query=%24.municipalities&label=municipalit%C3%A9s&color=ff5148&style=flat-square)](https://www.vrai-prix.com/api/stats)
21 −[![Logements](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.vrai-prix.com%2Fapi%2Fstats&query=%24.dwellings&label=logements&color=ff5148&style=flat-square)](https://www.vrai-prix.com/api/stats)
22 −[![Valeur médiane 2026](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.vrai-prix.com%2Fapi%2Fstats&query=%24.value_median_2026&label=m%C3%A9diane%202026&suffix=%20%24&color=ff5148&style=flat-square)](https://www.vrai-prix.com/api/stats)
23 −[![Croissance](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.vrai-prix.com%2Fapi%2Fstats&query=%24.growth_2021_2026_pct&label=croissance%202021%E2%86%922026&suffix=%20%25&color=ff5148&style=flat-square)](https://www.vrai-prix.com/api/stats)
24 −![MdAPE](https://img.shields.io/badge/MdAPE-11%2C0%20%25-ff5148?style=flat-square)
25 −
13 +![Site](https://img.shields.io/badge/site-www.uqo--eval.app-006085?style=flat-square)
14 +![UQO](https://img.shields.io/badge/UQO-PMS%207469-006085?style=flat-square)
26 15 ![Next.js](https://img.shields.io/badge/Next.js-16.3-141814?style=flat-square&logo=nextdotjs)
27 16 ![React](https://img.shields.io/badge/React-19-141814?style=flat-square&logo=react)
28 17 ![TypeScript](https://img.shields.io/badge/TypeScript-strict-141814?style=flat-square&logo=typescript)
29 18 ![Tailwind](https://img.shields.io/badge/Tailwind-4-141814?style=flat-square&logo=tailwindcss)
30 19 ![SQLite](https://img.shields.io/badge/SQLite-FTS5-141814?style=flat-square&logo=sqlite)
31 20 ![Mapbox](https://img.shields.io/badge/carte-Mapbox%20GL-141814?style=flat-square&logo=mapbox)
32 −![Claude](https://img.shields.io/badge/agent%20Ka-Claude-141814?style=flat-square&logo=anthropic)
33 −![Vitest](https://img.shields.io/badge/tests-Vitest-141814?style=flat-square&logo=vitest)
34 −![ngrok](https://img.shields.io/badge/tunnel-ngrok-140a3c?style=flat-square&logo=ngrok)
35 −![Groupe KA](https://img.shields.io/badge/Groupe-KA-b7f000?style=flat-square)
36 −![remote-first](https://img.shields.io/badge/remote--first-spbgit-24292f?style=flat-square&logo=git&logoColor=white)
21 +![Claude](https://img.shields.io/badge/assistante%20Éva-Claude-141814?style=flat-square&logo=anthropic)
22 +![MdAPE](https://img.shields.io/badge/MdAPE-11%2C0%20%25-2BCCAE?style=flat-square)
37 23
38 24 </div>
39 25
40 −Vrai-Prix estime la valeur marchande de **n'importe quelle propriété du Québec** — unifamiliale, plex, condo, chalet, terrain — et montre *pourquoi* : chaque comparable, chaque ajustement en dollars, la pondération du modèle, la fourchette (P10–P90) et l'indice de confiance (A–D). Le différenciateur est la **transparence totale** : pas de score magique, pas de boîte noire. La méthodologie complète (normes **IAAO**) est publiée intégralement sur le site.
41 −
42 −Sous le capot : les **6 millésimes (2021-2026)** des rôles d'évaluation foncière géoréférencés du Québec (**MAMH**, données ouvertes) — **3 747 008 propriétés**, soit **22 150 285 observations** — fusionnés spatialement avec **745 119 transactions réelles** (99,88 % d'appariement, distance médiane 0,6 m), alimentant un **modèle hédonique LightGBM** (MdAPE 11,0 %, ratio médian 0,995 en holdout) et un **moteur de comparables ajustés**. Pour qui ? Propriétaires, acheteurs, courtiers et curieux qui veulent une estimation honnête et vérifiable, gratuite, sur les **1 097 municipalités** de la province.
43 −
44 −## Chiffres clés (live sur [/api/stats](https://www.vrai-prix.com/api/stats), vérifiés au 2026-08-24)
45 −
46 −| Métrique | Valeur |
47 −|---|---|
48 −| Propriétés (unités d'évaluation) | **3 747 008** |
49 −| Observations (6 millésimes 2021-2026) | **22 150 285** |
50 −| Transactions réelles appariées | **745 119** (99,88 %, distance médiane 0,6 m) |
51 −| Logements couverts | **4 400 551** |
52 −| Municipalités | **1 097** |
53 −| Valeur totale du parc 2026 | **2 009 452 277 400 $** (≈ 2,01 billions $) |
54 −| Valeur médiane 2026 | **444 300 $** |
55 −| Croissance 2021 → 2026 | **+43,1 %** |
56 −| Modèle | hédonique LightGBM + comparables ajustés — **MdAPE 11,0 %**, ratio médian 0,995 |
57 −| Millésime de la base | 2026 (dernier recalcul : 2026-08-08) |
58 −
59 −## Visite guidée
60 −
61 −*Captures du 2026-08-25 — refonte v2 « terminal éditorial » (mobile 390×844 · desktop 1440×900).*
62 −
63 −### Mobile
64 −
65 −<table>
66 − <tr>
67 − <td align="center"><img src="docs/screenshots/mobile/home.webp" width="240" alt="Accueil mobile"><br><sub><b>Accueil — recherche d'adresse, KA Tabbar</b></sub></td>
68 − <td align="center"><img src="docs/screenshots/mobile/carte.webp" width="240" alt="Carte mobile"><br><sub><b>Vrai-Prix Maps — pastilles d'estimation 3D</b></sub></td>
69 − <td align="center"><img src="docs/screenshots/mobile/estimation.webp" width="240" alt="Fiche mobile"><br><sub><b>Rapport de valeur — grands chiffres</b></sub></td>
70 − </tr>
71 − <tr>
72 − <td align="center"><img src="docs/screenshots/mobile/municipalite.webp" width="240" alt="Municipalité mobile"><br><sub><b>Page municipalité</b></sub></td>
73 − <td align="center"><img src="docs/screenshots/mobile/stats.webp" width="240" alt="Stats mobile"><br><sub><b>Statistiques provinciales</b></sub></td>
74 − <td align="center"><img src="docs/screenshots/mobile/ka.webp" width="240" alt="Agent Ka mobile"><br><sub><b>Ka · agent IA (widget v4 plein écran)</b></sub></td>
75 − </tr>
76 −</table>
77 −
78 −### Desktop
79 −
80 −<table>
81 − <tr>
82 − <td align="center"><img src="docs/screenshots/desktop/home.webp" width="420" alt="Accueil desktop"><br><sub><b>Accueil — hero encre, recherche centrale, graphique marché multi-séries</b></sub></td>
83 − <td align="center"><img src="docs/screenshots/desktop/estimation.webp" width="420" alt="Rapport de valeur"><br><sub><b>Rapport de valeur — fourchette honnête P10–P90, indice de confiance A–D</b></sub></td>
84 − </tr>
85 − <tr>
86 − <td align="center"><img src="docs/screenshots/desktop/estimation-details.webp" width="420" alt="Comparables"><br><sub><b>Fiche — comparables et chaîne de calcul montrée</b></sub></td>
87 − <td align="center"><img src="docs/screenshots/desktop/carte.webp" width="420" alt="Vrai-Prix Maps desktop"><br><sub><b>Vrai-Prix Maps — recherche par carte (Mapbox GL 3D)</b></sub></td>
88 − </tr>
89 − <tr>
90 − <td align="center"><img src="docs/screenshots/desktop/municipalite.webp" width="420" alt="Municipalité"><br><sub><b>Page municipalité — agrégats et palmarès locaux</b></sub></td>
91 − <td align="center"><img src="docs/screenshots/desktop/stats.webp" width="420" alt="Stats"><br><sub><b>Statistiques provinciales — flux de ventes prix-d'abord</b></sub></td>
92 − </tr>
93 − <tr>
94 − <td align="center"><img src="docs/screenshots/desktop/parc.webp" width="420" alt="Parc immobilier"><br><sub><b>Parc immobilier — explorateur du rôle d'évaluation</b></sub></td>
95 − <td align="center"><img src="docs/screenshots/desktop/ka.webp" width="420" alt="Agent Ka"><br><sub><b>Ka · agent IA — cartes d'annonces cliquables (ka-card v4)</b></sub></td>
96 − </tr>
97 −</table>
26 +**UQO Éval** est une plateforme universitaire qui permet aux étudiantes et étudiants de **comprendre comment une propriété est évaluée** — pas seulement d'obtenir un prix. Chaque estimation montre son calcul en entier : comparables retenus (et pourquoi), ajustements en dollars, pondération modèle/comparables, intervalle de confiance P10-P90, indice de confiance A-D — plus un **mode étudiant** qui décompose les formules du moteur.
98 27
99 −### Nouveautés front-end (2026-08-25)
28 +Sous le capot : les **6 millésimes (2021-2026)** des rôles d'évaluation foncière géoréférencés du Québec (**MAMH**, données ouvertes) — **3 747 008 propriétés**, 22,15 M d'observations — fusionnés spatialement avec **745 119 transactions réelles**, alimentant un **modèle hédonique LightGBM** (MdAPE 11,0 %, ratio médian 0,995) et un **moteur de comparables ajustés**. Validation selon les normes **IAAO**.
100 29
101 −- **Refonte v2 « terminal éditorial »** — hero encre pleine largeur, recherche centrale CTA rouge, graphique marché multi-séries interactif, rapport de valeur recomposé (grands chiffres, sections à filets), méthodologie en article.
102 −- **KA Tabbar v1** — barre de navigation mobile commune Groupe KA (Estimer · Marché · Ventes · Méthode).
103 −- **Header réellement collant** au défilement + nav mobile v2 plein écran.
104 −- **Widget ka-agent v4** sur `/ka` — cartes d'annonces cliquables (`ka-card`) et choix de clarification en boutons (`ka-choix`), bulle → plein écran.
30 +> ⚠️ **Disclaimer** — Plateforme à vocation **pédagogique, universitaire et expérimentale**. Les estimations sont statistiques et ne constituent pas une évaluation immobilière professionnelle certifiée (OEAQ).
105 31
106 −> 🗄️ Les captures d'époque (v1) sont conservées dans [`docs/archive/`](docs/archive/).
107 −
108 −### Guide pas à pas (« Utiliser le site en 5 étapes », depuis [/doc](https://www.vrai-prix.com/doc/index.html))
32 +## Fonctionnalités
109 33
110 −<table>
111 − <tr>
112 − <td align="center"><img src="public/doc/img/etape1.png" width="420" alt="Étape 1 — accueil de Vrai-Prix"><br><sub><b>Étape 1 — Arriver sur l'accueil</b></sub></td>
113 − <td align="center"><img src="public/doc/img/etape2.png" width="420" alt="Étape 2 — recherche d'adresse avec suggestions"><br><sub><b>Étape 2 — Chercher l'adresse</b></sub></td>
114 − </tr>
115 − <tr>
116 − <td align="center"><img src="public/doc/img/etape3.png" width="420" alt="Étape 3 — fiche d'estimation avec fourchette et indice de confiance"><br><sub><b>Étape 3 — Lire la fiche d'estimation</b></sub></td>
117 − <td align="center"><img src="public/doc/img/etape4.png" width="420" alt="Étape 4 — recherche par carte 3D"><br><sub><b>Étape 4 — Explorer par la carte</b></sub></td>
118 − </tr>
119 − <tr>
120 − <td align="center" colspan="2"><img src="public/doc/img/etape5.png" width="420" alt="Étape 5 — tableau de bord statistiques"><br><sub><b>Étape 5 — Aller plus loin : statistiques, parc, agent Ka</b></sub></td>
121 − </tr>
122 −</table>
34 +- **Analyser une propriété** — recherche plein-texte (FTS5) parmi 3,7 M d'unités ; estimation hybride 65 % modèle hédonique + 35 % comparables ajustés, fourchette P10-P90, indice A-D.
35 +- **Mode étudiant 🎓** — sur chaque fiche : pourquoi ces comparables, formule de pondération (3 gaussiennes), les 3 ajustements expliqués, combinaison 65/35 avec les vrais chiffres, décomposition du score de confiance, concepts (valeur marchande vs prix demandé vs prix de transaction vs rôle).
36 +- **Comparables expliqués** — carte radar + tableau : distance, date, prix, $/m², ajustements en dollars, poids.
37 +- **UQO Éval Maps** — recherche par carte 3D (Mapbox GL), pastilles de valeurs estimées.
38 +- **Éva · assistante pédagogique** — agent conversationnel (Claude, streaming SSE, 12 outils serveur) qui évalue et **explique** chaque calcul.
39 +- **Rapports PDF vectoriels** — standard (3 p.), professionnel (6 p., structure de rapport d'évaluation), parc, statistiques — tous avec **logo officiel UQO** et mention des cours IMM1003 · IMM1033.
40 +- **Méthodologie publique** (9 sections) — données, fusion spatiale, modèle, validation IAAO, moteur, **guide pédagogique des méthodes d'évaluation** (comparables, hédonique, coût, AVM, biais et limites).
41 +- **Statistiques provinciales**, pages municipalités (1 097), parc immobilier multi-adresses, bilingue FR/EN.
123 42
124 −## Fonctionnalités
43 +## Identité visuelle
125 44
126 −- **Estimation par adresse** — recherche plein-texte (SQLite FTS5) parmi 3 747 008 unités d'évaluation, tolérante aux fautes ; **plan B sans adresse** : estimation manuelle par municipalité + caractéristiques.
127 −- **Estimation hybride** — 65 % modèle hédonique LightGBM + 35 % comparables ajustés (marché, superficie, âge), fourchette P10–P90 par régression quantile, indice de confiance A–D obligatoire.
128 −- **Portrait de la propriété** — façade SVG dessinée par les données du registre, terrain à l'échelle, composition de la valeur ; historique 2021→2026 (six millésimes, badges YoY).
129 −- **Vrai-Prix Maps** — recherche par carte 3D (Mapbox GL via le framework partagé `@groupe-ka/ka-maps`), grappes de valeurs estimées pilotées par le viewport ; plan des comparables « arpenteur » en SVG pur.
130 −- **Ka · agent IA** — agent conversationnel (Claude, SDK Anthropic, streaming SSE) branché sur **12 outils serveur** : `evaluer_propriete`, `chercher_propriete`, `chercher_proprietes_secteur`, `comparables_detailles`, `comparer_proprietes`, `estimation_manuelle`, `evaluer_parc`, `dossier_complet`, `indice_marche`, `stats_municipalite`, `stats_provinciales`, `liens_rapports`.
131 −- **Rapports PDF 100 % vectoriels** (pdfkit) — standard (3 p.), professionnel bancaire (6 p.), parc consolidé, statistique provincial.
132 −- **Parc immobilier** — évaluation multi-adresses agrégée (jusqu'à 40 propriétés).
133 −- **Statistiques provinciales** — valeur totale du parc (2,01 billions $ au millésime 2026), palmarès des 200 municipalités, tableau de bord `/stats` + rapports PDF v3 personnalisés (catalogue de blocs, ReportBuilder).
134 −- **Contexte marché** — indice de marché, fourchette visuelle, accueil enrichi (upgrade 2026-08-22).
135 −- **SEO programmatique — 3,76 M d'URLs indexables** : 1 097 pages municipalités + ~5 900 pages municipalité × type + 3,75 M fiches d'estimation SSR avec métadonnées uniques, JSON-LD, sitemaps générés à la volée.
136 −- **KA ID** — connexion unique du Groupe KA (SSO signé via le hub groupe-ka.com, sans base d'utilisateurs) + favoris « Mon univers Ka ».
137 −- **Bilingue FR/EN**, méthodologie publique (7 sections, IAAO), conformité Loi 25, app compagnon iOS SwiftUI (dépôt séparé).
45 +Couleurs et logo **officiels de l'UQO** (Guide de normes graphiques, déc. 2025 — `assets/uqo/BRANDING.md`) :
46 +bleu institutionnel **PMS 7469 `#006085`**, turquoise web `#2BCCAE`, corail `#EA6047`. Logos officiels (EPS/SVG/PNG, versions couleur/blanc/noir) téléchargés depuis [uqo.ca/medias/logos](https://uqo.ca/medias/logos) — jamais redessinés ni déformés.
138 47
139 48 ## Architecture
140 49
@@ -147,168 +56,35 @@ Rôles d'évaluation MAMH 2021-2026 (6 millésimes × ~3,7 M unités)
147 56 ▼
148 57 Application Next.js 16 (App Router) — SQLite better-sqlite3 (units + FTS5
149 58 + transactions + indice de marché) · moteur de comparables (src/lib/engine.ts)
150 −· Ka Maps (Mapbox GL) · agent Ka (Claude + 12 outils) · rapports pdfkit
151 −· SSO KA ID · SEO programmatique
59 +· mode étudiant (src/components/StudentPanel.tsx) · Éva (Claude + 12 outils)
60 +· rapports pdfkit (logo UQO + mention IMM1003/IMM1033) · SEO programmatique
152 61 ```
153 62
154 −- **Frontend / API** : Next.js **16.3** (App Router), React **19**, TypeScript strict, Tailwind CSS **4** — tout est servi par une seule app (pages SSR + routes `/api/*`).
155 −- **Base de données** : SQLite (`data/vraiprix.db`, ~1,5 Go, non versionnée) via **better-sqlite3** — 3,75 M unités, FTS5, index spatiaux, mode WAL.
156 −- **Modèle** : pipeline externe LightGBM / scikit-learn (réentraînement : `scripts/hedonic-retrain.py`), estimations pré-calculées injectées dans la base (dernière ré-estimation sur le nœud : 2026-08-22, terrain/autre + recalibrage P10/P90).
157 −- **Agent Ka** : SDK Anthropic (`@anthropic-ai/sdk`), streaming SSE, 12 outils serveur, garde anti-hallucination sur les liens PDF.
158 −- **PDF** : pdfkit (rapports 100 % vectoriels, aucune capture d'écran).
159 −- **Tests** : Vitest (moteur d'estimation, `npm run test`) + Playwright (captures : `scripts/screenshots.mjs`).
160 −
161 −## API — endpoints principaux (21 routes)
162 −
163 −| Endpoint | Méthode | Rôle |
164 −|---|---|---|
165 −| `/api/search` | GET | recherche plein-texte d'adresses (FTS5, tolérante aux fautes) |
166 −| `/api/estimate` | GET | estimation hybride d'une unité (modèle + comparables, P10–P90, confiance A–D) |
167 −| `/api/nearby` | GET | unités voisines / grappes pour la carte |
168 −| `/api/ka` | POST | agent Ka (Claude, streaming SSE, 12 outils serveur) |
169 −| `/api/portfolio` | POST | évaluation de parc immobilier multi-adresses (≤ 40) |
170 −| `/api/report` | GET | rapport PDF standard (3 p.) |
171 −| `/api/report/pro` | GET | rapport PDF professionnel bancaire (6 p.) |
172 −| `/api/report/portfolio` | GET | rapport PDF de parc consolidé |
173 −| `/api/report/stats` | GET | rapport PDF statistique provincial |
174 −| `/api/stats` | GET | agrégats provinciaux publics (source des pastilles dynamiques ci-dessus) |
175 −| `/api/stats/catalog` · `/api/stats/dashboard` | GET | catalogue de blocs + tableau de bord (kit stats v3 Groupe KA) |
176 −| `/api/stats/report` · `/api/stats/report/custom` | GET/POST | rapports PDF v3 (modèles + personnalisés) |
177 −| `/api/auth/ka/login` · `/api/auth/ka/callback` | GET | SSO KA ID (hub groupe-ka.com) |
178 −| `/api/auth/me` · `/api/auth/logout` | GET/POST | session KA ID |
179 −| `/api/favorites` · `/api/favorites/toggle` | GET/POST | favoris « Mon univers Ka » |
180 −| `/api/lead` | POST | prise de contact / lead |
181 −
182 −## Données & conformité
183 −
184 −| Source | Contenu | Volume | Mise à jour |
185 −|---|---|---|---|
186 −| **MAMH — rôles d'évaluation foncière géoréférencés** (données ouvertes, Données Québec) | 6 millésimes 2021-2026, caractéristiques complètes des unités | 3 747 008 propriétés · 22 150 285 observations | à chaque millésime annuel (réingestion `scripts/`) |
187 −| **Transactions réelles** (fusion spatiale XY, EPSG:32198) | prix de vente appariés aux unités (99,88 %, distance médiane 0,6 m) | 745 119 transactions | lors du réentraînement du modèle |
188 −| **Connecteur « nouvelles ventes »** (JdM / api.qub.ca) | ingestion incrémentale de transactions récentes | à la demande | `scripts/ingest-jdm.mjs` (process PM2 `vrai-prix-ingest`) |
189 −| **Modèle hédonique LightGBM** | log-prix, 18 variables, quantiles P10/P90 — MdAPE 11,0 %, ratio médian 0,995 | estimations pré-calculées sur ~3,75 M unités | réentraînement `scripts/hedonic-retrain.py` (hors ligne) |
190 −
191 −- Contrairement aux apps à connecteurs du Groupe KA, Vrai-Prix ne dépend d'aucune synchronisation horaire : la base est **pré-calculée** et versionnée par millésime — l'app sert les estimations en lecture seule.
192 −- **Méthodologie publique** (7 sections, normes IAAO) publiée intégralement sur le site ; chaque estimation affiche ses comparables, ses ajustements et son indice de confiance.
193 −- **Disclaimer** : estimations statistiques à titre indicatif — elles ne remplacent pas une évaluation par un évaluateur agréé (**OEAQ**).
194 −- **Vie privée** : conformité Loi 25 ; SSO KA ID sans base d'utilisateurs locale ; analytics première partie (admin-ka `/collect`), aucun script publicitaire (Monetag retiré le 2026-08-23).
195 −
196 −## Structure du repo
197 −
198 63 | Répertoire | Rôle |
199 64 |---|---|
200 −| `src/app/` | pages App Router (accueil, `estimation/[id]`, `municipalite/[slug]/[type]`, `carte`, `ka`, `parc`, `stats`, `favoris`, sitemaps/robots) + routes `/api/*` |
201 −| `src/components/` | SearchBox, ResultView, MetricViz, RadarMap, KaCarte, KaChat, StatsView… |
202 −| `src/lib/` | moteur de comparables (`engine.ts`), accès DB, agrégats municipalités, agent Ka (`ka/`), SSO KA ID, générateurs PDF (`report*.ts`), SEO, i18n |
203 −| `data/` | base SQLite `vraiprix.db` (non versionnée) |
204 −| `scripts/` | réentraînement hédonique, build des stats, ingestion (dont `ingest-jdm.mjs`), captures Playwright |
205 −| `docs/` | pipeline de données (`PIPELINE-DONNEES.md`), notes Ka Maps, captures d'écran |
206 −| `public/` | assets statiques (favicons, OG, polices) + guide `/doc` (page, images, PDF) |
207 −| `ios/` | ressources de l'app compagnon iOS (SwiftUI) |
208 −| `assets/` | matériel graphique du projet |
65 +| `src/app/` | pages App Router (accueil, `estimation/[id]`, `carte`, `assistant`, `parc`, `stats`, `methodologie`, municipalités) + routes `/api/*` |
66 +| `src/components/` | SearchBox, ResultView (+ mode étudiant), StudentPanel, MetricViz, RadarMap, KaCarte, UqoFooter… |
67 +| `src/lib/` | moteur (`engine.ts`), accès DB, agent Éva (`ka/`), générateurs PDF (`report*.ts`), SEO, i18n |
68 +| `data/` | base SQLite `vraiprix.db` (2 Go, non versionnée) |
69 +| `assets/uqo/` | logos et normes graphiques officiels UQO |
70 +| `vendor/` | tarball `@groupe-ka/ka-maps` (framework de carte, vendorisé) |
209 71
210 72 ## Démarrage rapide
211 73
212 74 ```bash
213 −ssh M3U96a && cd ~/apps/vrai-prix
214 −npm install # dépendances (au besoin)
215 −npm run test # Vitest — moteur d'estimation (src/lib/engine.test.ts)
216 −npm run lint # ESLint 9 + config Next
217 −npm run dev # serveur de développement (next dev)
218 −npm run build # build de production
219 −npm start # next start (en prod : PM2, voir Déploiement)
75 +npm install
76 +npm run test # Vitest — moteur d'estimation
77 +npm run lint
78 +npm run dev # http://localhost:3000
79 +npm run build && npm start
220 80 ```
221 81
222 −## Développement (remote-first)
82 +Variables (`.env.local`) : `NEXT_PUBLIC_SITE_URL`, `VRAIPRIX_DB` (chemin de la base), `ANTHROPIC_API_KEY` (assistante Éva), `NEXT_PUBLIC_MAPBOX_TOKEN` (facultatif, défaut embarqué).
223 83
224 −⚠️ **La source de vérité est le repo git sur le nœud M3U96a** (`~/apps/vrai-prix`) — on n'édite **jamais** les copies laptop. Toute modification se fait sur le nœud via SSH : édition, build, redémarrage, puis commit + push **depuis le nœud**.
84 +## Provenance
225 85
226 −- Remote **`origin` = spbgit** (git perso, https://git.spboucher.ai) — sur M3U96a, l'origin est le chemin local **`/Users/simon-pierreboucher/srv/git/vrai-prix.git`** (bare repo) ;
227 −- remote secondaire `github` → github.com/spboucher-ai/vrai-prix.
228 −
229 −```bash
230 −cd ~/apps/vrai-prix
231 −npm run build # après changement de code
232 −pm2 restart vrai-prix
233 −git add <fichiers> && git commit -m "…" && git push origin main
234 −```
235 −
236 −## Variables d'environnement
237 −
238 −Noms seulement — **aucun secret n'est versionné** (`.env.local` sur le nœud).
239 −
240 −| Variable | Rôle |
241 −|---|---|
242 −| `VRAIPRIX_DB` | chemin de la base SQLite (`data/vraiprix.db`) |
243 −| `NEXT_PUBLIC_SITE_URL` | URL canonique du site (SEO, sitemaps) |
244 −| `NEXT_PUBLIC_MAPBOX_TOKEN` | carte 3D Vrai-Prix Maps (jeton public) |
245 −| `ANTHROPIC_API_KEY` | agent Ka (Claude) |
246 −| `KA_SSO_SECRET` · `KA_AUTH_SECRET` | signature SSO / session KA ID |
247 −| `KA_HUB_URL` · `KA_BASE_URL` | hub groupe-ka.com + URL de rappel du site |
248 −
249 −## Déploiement
250 −
251 −| | |
252 −|---|---|
253 −| **Nœud** | **M3U96a** (Mac Studio M3 Ultra, cluster MacLustr) — `~/apps/vrai-prix` |
254 −| **Port** | **8090** |
255 −| **Domaine** | [www.vrai-prix.com](https://www.vrai-prix.com) via tunnel ngrok |
256 −
257 −| Processus PM2 | Commande réelle | Rôle |
258 −|---|---|---|
259 −| `vrai-prix` | `next start -H 0.0.0.0 -p 8090` | l'app Next.js — pages SSR + API |
260 −| `vrai-prix-ngrok` | `ngrok http --url=www.vrai-prix.com 8090` | tunnel vers www.vrai-prix.com |
261 −| `vrai-prix-ingest` | `node scripts/ingest-jdm.mjs` | connecteur incrémental « nouvelles ventes » — lancé à la demande, arrêté au repos |
262 −
263 −```bash
264 −# sur M3U96a, après un changement :
265 −cd ~/apps/vrai-prix && npm run build && pm2 restart vrai-prix
266 −pm2 logs vrai-prix --lines 30
267 −```
268 −
269 −## Documentation
270 −
271 −- **Guide utilisateur en ligne** : [www.vrai-prix.com/doc/index.html](https://www.vrai-prix.com/doc/index.html) — à quoi sert le site, utilisation en 5 étapes, provenance des données, FAQ.
272 −- **Guide PDF téléchargeable** : [vrai-prix-documentation.pdf](https://www.vrai-prix.com/doc/vrai-prix-documentation.pdf).
273 −- **Méthodologie complète** (normes IAAO, 7 sections) : publiée sur le site.
274 −- **Docs internes** : `docs/` (pipeline de données `PIPELINE-DONNEES.md`, notes Ka Maps, captures).
275 −
276 −## Historique
277 −
278 −| Date | Jalon |
279 −|---|---|
280 −| 2026-08-08 | naissance du projet ; 08-09 : moteur d'évaluation transparent v1 + agent Ka (Claude, streaming, 11 puis 12 outils) |
281 −| 2026-08-13 | Vrai-Prix Maps (framework Ka Maps), SSO KA ID, SEO programmatique (sitemaps 3,7 M de fiches) |
282 −| 2026-08-17 | harmonisation Groupe KA (ka-ui, footer commun) + API publique `/api/stats` + assets OG |
283 −| 2026-08-19 | stats v2 (`/stats` ultra complet + 5 rapports PDF), KA Agent v2 plein écran, socle mobile |
284 −| 2026-08-22 | ré-estimation hédonique sur le nœud (terrain/autre + recalibrage P10/P90), connecteur « nouvelles ventes » JdM, upgrade apparence (contexte marché, fourchette visuelle) |
285 −| 2026-08-23/24 | stats v3 (rapports PDF personnalisés), favoris « Mon univers Ka », page `/doc` + PDF, README v3 |
286 −
287 −## Écosystème Groupe KA
288 −
289 −| Site | Univers |
290 −|---|---|
291 −| [groupe-ka.com](https://www.groupe-ka.com) | portail |
292 −| [lou-ka.com](https://www.lou-ka.com) | logements à louer |
293 −| [immo-ka.com](https://www.immo-ka.com) | propriétés à vendre |
294 −| [vrai-prix.com](https://www.vrai-prix.com) | estimation immobilière |
295 −| [auto-ka.com](https://www.auto-ka.com) | véhicules |
296 −| [fabri-ka.com](https://www.fabri-ka.com) | produits québécois |
297 −| [food-ka.com](https://www.food-ka.com) | épicerie / alimentation |
298 −| [resto-ka.com](https://www.resto-ka.com) | restaurants |
299 −| [sorti-ka.com](https://www.sorti-ka.com) | sorties et événements |
300 −| [job-ka.com](https://www.job-ka.com) | emplois |
301 −| [crea-ka.com](https://www.crea-ka.com) | créateurs |
302 −| [trouve-ka.com](https://www.trouve-ka.com) | petites annonces |
303 −| [api-ka.com](https://www.api-ka.com) | API de données |
304 −
305 −## Contact
306 −
307 −**Simon-Pierre Boucher** — fondateur, Groupe KA
308 −📧 [contact@spboucher.ai](mailto:contact@spboucher.ai)
86 +Projet dérivé du moteur d'estimation **Vrai-Prix** (même socle de données et de modèles), entièrement ré-orienté et re-marqué en plateforme pédagogique universitaire indépendante. La production Vrai-Prix n'est ni modifiée ni partagée : base de données, environnement, domaine, nœud et processus sont distincts.
309 87
310 88 ---
311 89
312 −© Groupe KA — Simon-Pierre Boucher · [contact@spboucher.ai](mailto:contact@spboucher.ai)
313 −
314 −*Ce dépôt vit sur **spbgit** (git perso — https://git.spboucher.ai). Estimations statistiques à titre indicatif — elles ne remplacent pas une évaluation par un évaluateur agréé (OEAQ).*
90 +© UQO Éval — laboratoire pédagogique. Estimations statistiques à titre indicatif ; elles ne remplacent pas une évaluation par un évaluateur agréé (OEAQ).
added assets/uqo/BRANDING.md +137 −0
@@ -0,0 +1,137 @@
1 +# UQO — Identité visuelle officielle
2 +
3 +Université du Québec en Outaouais (uqo.ca)
4 +Sources : page officielle « Outils de communication » (https://uqo.ca/medias/logos) et
5 +**Guide de normes graphiques UQO — Décembre 2025** (PDF officiel, inclus dans ce dossier).
6 +
7 +> Note importante : le logo officiel actuel de l'UQO est la signature « UQO » (wordmark)
8 +> en **bleu PMS 7469**, accompagnée ou non de la mention « Université du Québec en Outaouais ».
9 +> L'UQO n'utilise plus le symbole du réseau Université du Québec, et le vert historique
10 +> est explicitement interdit (« Ne jamais utiliser une autre couleur que le bleu officiel
11 +> de l'UQO » — le guide montre un logo vert comme premier contre-exemple, p. 10).
12 +
13 +---
14 +
15 +## Fichiers téléchargés
16 +
17 +Tous validés (`file`, taille, rendu visuel vérifié).
18 +
19 +### Sources vectorielles officielles (masters, packages ZIP officiels de la DCR)
20 +
21 +Provenance : https://uqo.ca/medias/logos → « Toutes les versions »
22 +- Logo simple : https://uqo.ca/docs/11069 → `31915149uqo-complet.zip`
23 +- Logo avec mention : https://uqo.ca/docs/11073 → `1232463876uqolong-complet.zip`
24 +- JPG HR mention : https://uqo.ca/docs/11076 → `1082944393uqolong-jpg-h.zip`
25 +
26 +| Fichier | Format | Contenu |
27 +|---|---|---|
28 +| `UQO_PMS7469.eps` | EPS vectoriel | Logo simple « UQO », couleur officielle (ton direct Pantone 7469) — **master couleur** |
29 +| `UQO_CMYK.eps` | EPS vectoriel | Logo simple, quadrichromie CMYK |
30 +| `UQO_BLANC.eps` | EPS vectoriel | Logo simple, blanc (fonds foncés) |
31 +| `UQO_NOIR.eps` | EPS vectoriel | Logo simple, noir (fonds pâles) |
32 +| `UQO_Complet_PMS7469.eps` | EPS vectoriel | Logo avec mention « Université du Québec en Outaouais », Pantone 7469 — **master couleur** |
33 +| `UQO_Complet_CMYK.eps` | EPS vectoriel | Logo avec mention, CMYK |
34 +| `UQO_Complet_BLANC.eps` | EPS vectoriel | Logo avec mention, blanc |
35 +| `UQO_Complet_NOIR.eps` | EPS vectoriel | Logo avec mention, noir |
36 +
37 +### Rasters officiels (extraits des mêmes ZIP)
38 +
39 +| Fichier | Dimensions | Contenu |
40 +|---|---|---|
41 +| `UQO_couleur_300dpi.jpg` | 348 × 143 px | Logo simple couleur, JPG « haute résolution » officiel |
42 +| `UQO_Complet_couleur_300dpi.jpg` | 442 × 216 px | Logo avec mention, couleur, JPG officiel |
43 +
44 +### PNG haute résolution (dérivés localement des EPS officiels, Ghostscript 600 dpi, fond transparent)
45 +
46 +| Fichier | Dimensions | Contenu |
47 +|---|---|---|
48 +| `UQO_couleur.png` | 2123 × 853 px | Logo simple couleur, transparent |
49 +| `UQO_Complet_couleur.png` | 2733 × 1312 px | Logo avec mention, couleur, transparent |
50 +| `UQO_Complet_noir.png` | 2733 × 1312 px | Logo avec mention, noir, transparent |
51 +
52 +### SVG (versions blanches utilisées par le header du site officiel uqo.ca)
53 +
54 +| Fichier | viewBox | Provenance | Contenu |
55 +|---|---|---|---|
56 +| `UQO-logo-w.svg` | 0 0 958.67 216 | https://uqo.ca/sites/default/files/UQO-logo-w.svg | Logo blanc horizontal avec mention (header desktop uqo.ca) — **idéal pour fond foncé sur le web** |
57 +| `logo_uqo_white.svg` | 0 0 754.84 303.29 | https://uqo.ca/sites/default/files/inline-images/logo_uqo_white.svg | Wordmark « UQO » seul, blanc (header mobile uqo.ca) |
58 +
59 +Il n'existe pas de SVG couleur publié par l'UQO ; pour le web en couleur, utiliser les PNG
60 +600 dpi ci-dessus, ou convertir les EPS. (Les SVG blancs ont un seul `fill:#FFFFFF` /
61 +`.cls-1{fill:#fff}` ; un recolorage en #005F85 reproduit le wordmark bleu, mais dans la
62 +version officielle avec mention le texte de la mention est gris #414042, pas bleu.)
63 +
64 +### Documentation
65 +
66 +| Fichier | Contenu |
67 +|---|---|
68 +| `GuideDeNormes_UQO.pdf` | Guide de normes graphiques UQO, déc. 2025, 21 p. (https://uqo.ca/docs/11068 → https://uqo.ca/sites/default/files/1889470224guide-de-normes-uqo.pdf) |
69 +
70 +---
71 +
72 +## Couleurs officielles
73 +
74 +### Couleur principale — « le bleu officiel de l'UQO »
75 +
76 +Source : Guide de normes graphiques UQO, p. 14 (« Couleurs »).
77 +
78 +| Usage | Valeur |
79 +|---|---|
80 +| Pantone | **PMS 7469** |
81 +| CMYK | C:100 M:57 Y:30 K:10 |
82 +| RGB (officiel, guide) | R:0 G:96 B:133 → **#006085** |
83 +| Hex mesuré dans l'EPS officiel (rendu) | **#005F85** |
84 +| Gris de la mention (mesuré dans l'EPS) | #414042 |
85 +| Bleu signature courriel (guide, p. 19) | #136482 (Arial bold 13 pt) |
86 +| Bleu primaire du site web uqo.ca (CSS, thème `uqo_radix`) | #005477 (déclinaison foncée : #0D3D52) |
87 +
88 +Utiliser #006085 (ou #005F85) comme bleu de marque ; #005477 si l'on veut coller au site web actuel.
89 +
90 +### Couleurs complémentaires (guide p. 14 — CMYK/RGB officiels, hex convertis)
91 +
92 +| Nom | RGB officiel | Hex |
93 +|---|---|---|
94 +| Bleu gris | 52 / 77 / 90 | #344D5A |
95 +| Vert | 88 / 128 / 93 | #58805D |
96 +| Beige | 215 / 185 / 151 | #D7B997 |
97 +| Gris vert | 199 / 222 / 214 | #C7DED6 |
98 +| Bleu turquoise | 43 / 204 / 174 | #2BCCAE (le site utilise #2ACCAE) |
99 +| Orange | 237 / 114 / 101 | #ED7265 (le site utilise #EA6047) |
100 +| Jaune | 235 / 199 / 64 | #EBC740 |
101 +
102 +### Typographie
103 +
104 +- Police institutionnelle : **Switzer** (titres Bold, intro Semibold, corps Regular) —
105 + gratuite sur https://www.fontshare.com/fonts/switzer
106 +- Signature courriel : Arial (bold 13 pt #136482 pour le nom, 10 pt #000000 pour le reste)
107 +
108 +---
109 +
110 +## Règles d'usage (Guide de normes graphiques, déc. 2025)
111 +
112 +- **Proportions** : le logo doit être présenté dans ses proportions originales ; il est
113 + **strictement interdit** de le modifier par addition, soustraction ou de toute autre manière.
114 +- **Zone de protection** (p. 8) : marge minimale tout autour égale à **l'intérieur de la
115 + lettre « U »** (une fois en hauteur et une fois en largeur). Aucune illustration, photo,
116 + texte ou graphique ne doit empiéter sur cette zone.
117 +- **Déclinaisons couleur** (p. 7) :
118 + - fond blanc ou trame ≤ 10 % → logo **bleu** (couleur officielle) ;
119 + - fond foncé → logo **blanc** ;
120 + - fond pâle → logo **noir** ;
121 + - impression 1 couleur → logo bleu.
122 +- **Contre-indications** (p. 10, 15 cas) : jamais d'autre couleur que le bleu officiel
123 + (ex. vert interdit), jamais d'intensité < 100 %, pas d'outline/stroke, pas de modification
124 + de disposition ni de proportion relative, pas d'ombre portée ni d'effet, pas de distorsion,
125 + pas d'enclave dans une forme, jamais intégré dans une phrase/slogan, pas sur fond à faible
126 + contraste, pas combiné à un autre visuel, pas incliné, pas de motif, pas sur une photo,
127 + pas combiné à un autre logo sans autorisation de la DCR.
128 +- **Simple vs complet** (p. 11) : dans la majorité des cas, utiliser le logo simple « UQO » ;
129 + la version avec mention complète est recommandée pour les documents officiels ou les
130 + publics qui ne connaissent pas l'UQO.
131 +- **Autorisation** : seules les unités administratives et le personnel de l'UQO peuvent
132 + utiliser le logo. Étudiants, associations et partenaires externes doivent obtenir une
133 + **autorisation écrite de la Direction des communications et du recrutement (DCR)**
134 + (maquette à soumettre pour approbation). Contact : uqo.ca/dcr, 819-595-3900.
135 +- Pour une qualité optimale, toujours partir des fichiers vectoriels EPS (réduire un grand
136 + format plutôt qu'agrandir un petit).
137 +- Signature courriel : logo en transparence, **145 px de large** (p. 19).
added assets/uqo/GuideDeNormes_UQO.pdf +0 −0

Binary file not shown.

added assets/uqo/UQO-logo-w.svg +146 −0
@@ -0,0 +1,146 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<!-- Generator: Adobe Illustrator 27.8.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3 +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4 + viewBox="0 0 958.67 216" style="enable-background:new 0 0 958.67 216;" xml:space="preserve">
5 +<style type="text/css">
6 + .st0{fill:#FFFFFF;}
7 +</style>
8 +<g>
9 + <g>
10 + <path class="st0" d="M569,60.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29s-2.23-3.88-2.82-6.2
11 + c-0.6-2.32-0.9-4.73-0.9-7.23V21.19h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
12 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
13 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06V21.19h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
14 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.33,1.76-3.04,3.14-5.14,4.16C574.56,59.54,572,60.05,569,60.05z"/>
15 + <path class="st0" d="M599.87,32.49v27.28h-6.08V21.19h5l21.85,27.88V21.24h6.14v38.53h-5.32L599.87,32.49z"/>
16 + <path class="st0" d="M635.47,59.78V21.19h6.08v38.59H635.47z"/>
17 + <path class="st0" d="M652.69,21.19l11.63,31.14l11.58-31.14h6.47l-15.16,38.59h-5.76l-15.22-38.59H652.69z"/>
18 + <path class="st0" d="M713.56,54.4v5.38H687.1V21.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23V54.4H713.56z"/>
19 + <path class="st0" d="M719.87,59.78V21.19h16.79c1.74,0,3.34,0.36,4.78,1.09c1.45,0.72,2.71,1.69,3.78,2.88
20 + c1.07,1.19,1.9,2.54,2.5,4.02c0.6,1.49,0.89,3.01,0.89,4.57c0,1.7-0.31,3.34-0.92,4.89c-0.62,1.56-1.49,2.9-2.64,4.02
21 + c-1.14,1.12-2.47,1.94-4,2.45l9.08,14.67h-6.85l-8.37-13.42h-8.97v13.42H719.87z M725.95,40.97h10.65c1.12,0,2.12-0.33,2.99-0.98
22 + c0.87-0.65,1.56-1.53,2.07-2.64c0.51-1.11,0.76-2.31,0.76-3.61c0-1.34-0.29-2.54-0.87-3.61c-0.58-1.07-1.33-1.93-2.26-2.58
23 + c-0.92-0.65-1.93-0.98-3.01-0.98h-10.33V40.97z"/>
24 + <path class="st0" d="M778.62,30.27c-0.36-0.4-0.89-0.82-1.58-1.28c-0.69-0.45-1.51-0.88-2.45-1.28c-0.94-0.4-1.96-0.72-3.05-0.98
25 + c-1.08-0.25-2.21-0.38-3.37-0.38c-2.54,0-4.42,0.47-5.65,1.41c-1.23,0.94-1.85,2.24-1.85,3.9c0,1.23,0.34,2.19,1.03,2.9
26 + c0.69,0.71,1.74,1.29,3.15,1.76c1.41,0.47,3.19,0.98,5.33,1.52c2.61,0.61,4.87,1.35,6.79,2.19c1.92,0.85,3.39,1.98,4.4,3.38
27 + c1.01,1.41,1.52,3.27,1.52,5.58c0,1.95-0.36,3.63-1.09,5.04c-0.72,1.41-1.74,2.55-3.04,3.44c-1.3,0.88-2.81,1.53-4.51,1.95
28 + c-1.7,0.41-3.53,0.62-5.49,0.62c-1.92,0-3.83-0.2-5.73-0.6c-1.9-0.4-3.71-0.99-5.43-1.77c-1.72-0.78-3.31-1.73-4.76-2.85
29 + l2.77-5.22c0.47,0.47,1.15,0.99,2.04,1.55s1.92,1.1,3.1,1.6c1.18,0.51,2.46,0.93,3.86,1.28c1.4,0.35,2.82,0.52,4.26,0.52
30 + c2.43,0,4.29-0.42,5.57-1.27c1.29-0.84,1.93-2.06,1.93-3.65c0-1.3-0.42-2.34-1.25-3.12c-0.83-0.78-2.05-1.44-3.64-1.98
31 + c-1.6-0.54-3.5-1.08-5.71-1.63c-2.54-0.69-4.66-1.43-6.36-2.25c-1.7-0.81-2.98-1.86-3.83-3.14c-0.85-1.28-1.28-2.92-1.28-4.91
32 + c0-2.53,0.61-4.67,1.85-6.42c1.23-1.75,2.91-3.07,5.03-3.96c2.12-0.88,4.5-1.33,7.15-1.33c1.74,0,3.38,0.19,4.94,0.57
33 + c1.56,0.38,3.02,0.9,4.38,1.55c1.36,0.65,2.6,1.4,3.72,2.23L778.62,30.27z"/>
34 + <path class="st0" d="M789,59.78V21.19h6.08v38.59H789z"/>
35 + <path class="st0" d="M832.09,26.57h-12.83v33.21h-6.14V26.57h-12.88v-5.38h31.85V26.57z"/>
36 + <path class="st0" d="M863.72,54.4v5.38h-26.46V21.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23V54.4H863.72z M852.91,17.17
37 + l-3.91-1.2l3.1-5.97h5.71L852.91,17.17z"/>
38 + <path class="st0" d="M552.91,112.78V74.19h13.7c4.17,0,7.64,0.86,10.43,2.58c2.79,1.72,4.88,4.04,6.28,6.96
39 + c1.4,2.91,2.1,6.15,2.1,9.7c0,3.91-0.77,7.32-2.31,10.22s-3.72,5.14-6.55,6.74c-2.82,1.6-6.14,2.39-9.94,2.39H552.91z
40 + M579.22,93.43c0-2.72-0.5-5.12-1.49-7.2c-1-2.08-2.43-3.71-4.29-4.89c-1.87-1.18-4.14-1.77-6.82-1.77H559v27.82h7.61
41 + c2.72,0,5.01-0.61,6.87-1.85c1.87-1.23,3.29-2.9,4.27-5C578.73,98.45,579.22,96.08,579.22,93.43z"/>
42 + <path class="st0" d="M607.91,113.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29s-2.23-3.88-2.82-6.2
43 + c-0.6-2.32-0.9-4.73-0.9-7.23V74.19h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
44 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
45 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06V74.19h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
46 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.33,1.76-3.04,3.14-5.14,4.16C613.47,112.54,610.92,113.05,607.91,113.05z"/>
47 + <path class="st0" d="M662.15,113.05c-2.79,0-5.31-0.55-7.55-1.66c-2.25-1.11-4.18-2.58-5.82-4.43c-1.63-1.85-2.89-3.94-3.78-6.28
48 + c-0.89-2.34-1.33-4.74-1.33-7.2c0-2.57,0.47-5.03,1.41-7.37c0.94-2.34,2.25-4.42,3.91-6.25c1.66-1.83,3.63-3.28,5.9-4.35
49 + c2.26-1.07,4.72-1.6,7.36-1.6c2.75,0,5.26,0.56,7.53,1.69c2.26,1.12,4.2,2.63,5.82,4.51s2.86,3.99,3.75,6.3
50 + c0.89,2.32,1.33,4.69,1.33,7.12c0,2.57-0.46,5.03-1.39,7.36c-0.93,2.34-2.22,4.42-3.89,6.25c-1.67,1.83-3.63,3.27-5.9,4.32
51 + C667.25,112.52,664.79,113.05,662.15,113.05z M662.2,107.56c1.96,0,3.7-0.4,5.22-1.19c1.52-0.79,2.8-1.88,3.83-3.23
52 + c1.04-1.36,1.82-2.88,2.37-4.57c0.54-1.69,0.81-3.38,0.81-5.08c0-1.81-0.28-3.57-0.84-5.27c-0.56-1.7-1.39-3.21-2.47-4.51
53 + c-1.09-1.3-2.38-2.35-3.89-3.13c-1.5-0.78-3.18-1.17-5.03-1.17c-1.95,0-3.69,0.4-5.22,1.19c-1.52,0.8-2.81,1.88-3.86,3.23
54 + c-1.05,1.36-1.86,2.87-2.42,4.54c-0.56,1.66-0.84,3.37-0.84,5.11c0,1.81,0.29,3.57,0.87,5.27c0.58,1.7,1.4,3.2,2.47,4.51
55 + c1.07,1.3,2.36,2.35,3.89,3.13C658.62,107.17,660.32,107.56,662.2,107.56z M662.97,99.35h5.87l11.74,13.42h-5.87L662.97,99.35z"/>
56 + <path class="st0" d="M702.96,113.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29c-1.29-1.81-2.23-3.88-2.82-6.2
57 + c-0.6-2.32-0.9-4.73-0.9-7.23V74.19h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
58 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
59 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06V74.19h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
60 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.32,1.76-3.04,3.14-5.14,4.16C708.52,112.54,705.97,113.05,702.96,113.05z"/>
61 + <path class="st0" d="M754.21,107.4v5.38h-26.46V74.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23v11.79H754.21z M743.4,70.17
62 + l-3.91-1.2l3.1-5.97h5.71L743.4,70.17z"/>
63 + <path class="st0" d="M790.84,102.83c0,2.07-0.53,3.84-1.6,5.33c-1.07,1.48-2.51,2.63-4.32,3.42c-1.81,0.79-3.81,1.19-5.98,1.19
64 + h-18.42V74.19h19.89c1.81,0,3.38,0.48,4.7,1.44c1.32,0.96,2.34,2.19,3.04,3.7c0.71,1.5,1.06,3.07,1.06,4.7
65 + c0,1.88-0.48,3.65-1.44,5.3c-0.96,1.65-2.33,2.87-4.1,3.67c2.21,0.65,3.96,1.84,5.24,3.56
66 + C790.2,98.28,790.84,100.37,790.84,102.83z M766.6,79.46v11.36h11.2c1.01,0,1.92-0.24,2.72-0.73c0.79-0.49,1.43-1.16,1.9-2.01
67 + c0.47-0.85,0.71-1.82,0.71-2.91c0-1.12-0.22-2.11-0.65-2.96c-0.43-0.85-1.02-1.52-1.77-2.01c-0.75-0.49-1.6-0.73-2.58-0.73H766.6z
68 + M784.6,101.69c0-1.08-0.23-2.07-0.68-2.96c-0.45-0.89-1.07-1.61-1.85-2.17c-0.78-0.56-1.69-0.84-2.74-0.84H766.6v11.79h12.34
69 + c1.08,0,2.06-0.26,2.91-0.79c0.85-0.53,1.52-1.23,2.01-2.12C784.35,103.71,784.6,102.74,784.6,101.69z"/>
70 + <path class="st0" d="M823.67,107.4v5.38H797.2V74.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23v11.79H823.67z"/>
71 + <path class="st0" d="M826.82,93.16c0-2.35,0.42-4.66,1.25-6.93s2.06-4.33,3.67-6.19c1.61-1.87,3.58-3.35,5.9-4.46
72 + c2.32-1.1,4.95-1.66,7.88-1.66c3.48,0,6.48,0.79,9.02,2.36c2.54,1.58,4.42,3.6,5.65,6.06l-4.78,3.21
73 + c-0.72-1.56-1.64-2.79-2.75-3.7c-1.11-0.9-2.3-1.54-3.59-1.9c-1.29-0.36-2.54-0.54-3.78-0.54c-2.03,0-3.81,0.41-5.32,1.22
74 + c-1.52,0.82-2.8,1.89-3.83,3.23c-1.03,1.34-1.81,2.84-2.34,4.51c-0.53,1.66-0.79,3.33-0.79,5c0,1.85,0.31,3.63,0.92,5.35
75 + c0.62,1.72,1.48,3.24,2.58,4.57c1.11,1.32,2.42,2.36,3.94,3.12c1.52,0.76,3.19,1.14,5,1.14c1.23,0,2.52-0.22,3.86-0.65
76 + c1.34-0.43,2.58-1.12,3.72-2.04c1.14-0.92,2.04-2.15,2.69-3.67l5.06,2.88c-0.8,1.92-2.04,3.55-3.72,4.89
77 + c-1.69,1.34-3.57,2.36-5.65,3.05c-2.08,0.69-4.14,1.03-6.17,1.03c-2.72,0-5.2-0.57-7.45-1.71c-2.25-1.14-4.18-2.66-5.81-4.57
78 + c-1.63-1.9-2.9-4.04-3.81-6.41C827.28,97.99,826.82,95.58,826.82,93.16z"/>
79 + <path class="st0" d="M579.38,160.4v5.38h-26.46v-38.59h25.98v5.38H559v10.98h17.23v5.05H559v11.79H579.38z"/>
80 + <path class="st0" d="M591.77,138.49v27.28h-6.08v-38.59h5l21.85,27.88v-27.83h6.14v38.53h-5.32L591.77,138.49z"/>
81 + <path class="st0" d="M656.83,166.05c-2.79,0-5.31-0.54-7.55-1.63c-2.25-1.08-4.18-2.56-5.82-4.43c-1.63-1.87-2.89-3.97-3.78-6.3
82 + c-0.89-2.34-1.33-4.74-1.33-7.2c0-2.57,0.47-5.03,1.41-7.37c0.94-2.34,2.25-4.42,3.91-6.25c1.66-1.83,3.63-3.28,5.9-4.35
83 + c2.26-1.07,4.72-1.6,7.36-1.6c2.75,0,5.26,0.56,7.53,1.69c2.26,1.12,4.2,2.63,5.82,4.51c1.61,1.88,2.86,3.99,3.75,6.3
84 + c0.89,2.32,1.33,4.69,1.33,7.12c0,2.57-0.46,5.03-1.39,7.36c-0.93,2.34-2.22,4.42-3.89,6.25c-1.67,1.83-3.63,3.27-5.9,4.32
85 + C661.92,165.52,659.47,166.05,656.83,166.05z M644.54,146.48c0,1.81,0.29,3.57,0.87,5.27c0.58,1.7,1.4,3.22,2.47,4.54
86 + c1.07,1.32,2.36,2.36,3.89,3.12c1.52,0.76,3.22,1.14,5.11,1.14c1.96,0,3.7-0.4,5.22-1.19c1.52-0.79,2.8-1.88,3.83-3.23
87 + c1.04-1.36,1.82-2.88,2.37-4.57c0.54-1.69,0.81-3.38,0.81-5.08c0-1.81-0.28-3.56-0.84-5.25c-0.56-1.69-1.39-3.19-2.47-4.51
88 + c-1.09-1.32-2.38-2.37-3.89-3.13c-1.5-0.76-3.18-1.14-5.03-1.14c-1.95,0-3.69,0.4-5.22,1.2c-1.52,0.79-2.81,1.87-3.86,3.2
89 + c-1.05,1.34-1.86,2.84-2.42,4.51C644.82,143.04,644.54,144.74,644.54,146.48z"/>
90 + <path class="st0" d="M697.75,166.05c-3.08,0-5.68-0.53-7.8-1.58s-3.82-2.48-5.11-4.29c-1.29-1.81-2.23-3.88-2.82-6.2
91 + c-0.6-2.32-0.9-4.73-0.9-7.23v-19.56h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
92 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
93 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06v-19.56h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
94 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.32,1.76-3.04,3.14-5.14,4.16C703.31,165.54,700.75,166.05,697.75,166.05z"/>
95 + <path class="st0" d="M750.85,132.57h-12.83v33.21h-6.14v-33.21H719v-5.38h31.85V132.57z"/>
96 + <path class="st0" d="M763.51,127.19h5.76l15,38.59h-6.41l-3.77-10.49h-15.43l-3.79,10.49h-6.47L763.51,127.19z M772.47,150.78
97 + l-6.08-16.9l-6.11,16.9H772.47z"/>
98 + <path class="st0" d="M803.62,166.05c-2.79,0-5.31-0.54-7.55-1.63c-2.25-1.08-4.18-2.56-5.82-4.43c-1.63-1.87-2.89-3.97-3.78-6.3
99 + c-0.89-2.34-1.33-4.74-1.33-7.2c0-2.57,0.47-5.03,1.41-7.37c0.94-2.34,2.25-4.42,3.91-6.25c1.66-1.83,3.63-3.28,5.9-4.35
100 + c2.26-1.07,4.72-1.6,7.36-1.6c2.75,0,5.26,0.56,7.53,1.69c2.26,1.12,4.2,2.63,5.82,4.51c1.61,1.88,2.86,3.99,3.75,6.3
101 + c0.89,2.32,1.33,4.69,1.33,7.12c0,2.57-0.46,5.03-1.39,7.36c-0.93,2.34-2.22,4.42-3.89,6.25c-1.67,1.83-3.63,3.27-5.9,4.32
102 + C808.71,165.52,806.26,166.05,803.62,166.05z M791.33,146.48c0,1.81,0.29,3.57,0.87,5.27c0.58,1.7,1.4,3.22,2.47,4.54
103 + c1.07,1.32,2.36,2.36,3.89,3.12c1.52,0.76,3.22,1.14,5.11,1.14c1.96,0,3.7-0.4,5.22-1.19c1.52-0.79,2.8-1.88,3.83-3.23
104 + c1.04-1.36,1.82-2.88,2.37-4.57c0.54-1.69,0.81-3.38,0.81-5.08c0-1.81-0.28-3.56-0.84-5.25c-0.56-1.69-1.39-3.19-2.47-4.51
105 + c-1.09-1.32-2.38-2.37-3.89-3.13c-1.5-0.76-3.18-1.14-5.03-1.14c-1.95,0-3.69,0.4-5.22,1.2c-1.52,0.79-2.81,1.87-3.86,3.2
106 + c-1.05,1.34-1.86,2.84-2.42,4.51C791.61,143.04,791.33,144.74,791.33,146.48z"/>
107 + <path class="st0" d="M844.54,166.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29c-1.29-1.81-2.23-3.88-2.82-6.2
108 + c-0.6-2.32-0.9-4.73-0.9-7.23v-19.56H834v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
109 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
110 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06v-19.56h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
111 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.33,1.76-3.04,3.14-5.14,4.16C850.1,165.54,847.55,166.05,844.54,166.05z"/>
112 + <path class="st0" d="M878.89,127.19h5.76l15,38.59h-6.41l-3.77-10.49h-15.43l-3.79,10.49h-6.47L878.89,127.19z M887.85,150.78
113 + l-6.08-16.9l-6.11,16.9H887.85z"/>
114 + <path class="st0" d="M904.43,165.78v-38.59h6.08v38.59H904.43z"/>
115 + <path class="st0" d="M942.04,136.27c-0.36-0.4-0.89-0.82-1.58-1.28c-0.69-0.45-1.51-0.88-2.45-1.28c-0.94-0.4-1.96-0.72-3.05-0.98
116 + c-1.08-0.25-2.21-0.38-3.37-0.38c-2.54,0-4.42,0.47-5.65,1.41c-1.23,0.94-1.85,2.24-1.85,3.9c0,1.23,0.34,2.19,1.03,2.9
117 + c0.69,0.71,1.74,1.29,3.15,1.76c1.41,0.47,3.19,0.98,5.33,1.52c2.61,0.61,4.87,1.35,6.79,2.19c1.92,0.85,3.39,1.98,4.4,3.38
118 + s1.52,3.27,1.52,5.58c0,1.95-0.36,3.63-1.09,5.04c-0.72,1.41-1.74,2.55-3.04,3.44c-1.3,0.88-2.81,1.53-4.51,1.95
119 + c-1.7,0.41-3.53,0.62-5.49,0.62c-1.92,0-3.83-0.2-5.73-0.6c-1.9-0.4-3.71-0.99-5.43-1.77c-1.72-0.78-3.31-1.73-4.76-2.85
120 + l2.77-5.22c0.47,0.47,1.15,0.99,2.04,1.55s1.92,1.1,3.1,1.6c1.18,0.51,2.46,0.93,3.86,1.28c1.4,0.35,2.82,0.52,4.26,0.52
121 + c2.43,0,4.29-0.42,5.57-1.27c1.29-0.84,1.93-2.06,1.93-3.65c0-1.3-0.42-2.34-1.25-3.12s-2.05-1.44-3.64-1.98
122 + c-1.6-0.54-3.5-1.08-5.71-1.63c-2.54-0.69-4.66-1.43-6.36-2.25c-1.7-0.81-2.98-1.86-3.83-3.14c-0.85-1.28-1.28-2.92-1.28-4.91
123 + c0-2.53,0.61-4.67,1.85-6.42c1.23-1.75,2.91-3.07,5.03-3.96c2.12-0.88,4.5-1.33,7.15-1.33c1.74,0,3.38,0.19,4.94,0.57
124 + c1.56,0.38,3.02,0.9,4.38,1.55c1.36,0.65,2.6,1.4,3.72,2.23L942.04,136.27z"/>
125 + </g>
126 +</g>
127 +<g>
128 + <path class="st0" d="M197.45,82.58c6.69-22.42,30.37-35.21,52.79-28.52c22.42,6.69,35.21,30.37,28.52,52.79
129 + c-5.55,18.59-22.79,30.55-41.29,30.28l38.37,28.56c18.32-9.74,32.9-26.52,39.3-47.98c12.7-42.54-11.5-87.32-54.04-100.02
130 + c-42.54-12.7-87.32,11.5-100.02,54.04c-10.01,33.55,2.93,68.48,29.71,87.98l44.94,33.45l0,0c17.7,13.65,31.15,11.87,36.39,10.51
131 + l29.44-8.83l-88.73-66.04C198.91,118.45,192.21,100.13,197.45,82.58z"/>
132 + <path class="st0" d="M408.09,14.15c-44.4,0-80.39,35.99-80.39,80.39c0,44.4,35.99,80.39,80.39,80.39
133 + c44.4,0,80.39-35.99,80.39-80.39C488.48,50.14,452.49,14.15,408.09,14.15z M408.09,136.96c-23.39,0-42.43-19.03-42.43-42.43
134 + c0-23.39,19.03-42.43,42.43-42.43c23.39,0,42.43,19.03,42.43,42.43C450.52,117.93,431.49,136.96,408.09,136.96z"/>
135 + <g>
136 + <g>
137 + <path class="st0" d="M109.85,24.34v80.71c0,4.45-0.45,8.42-1.32,11.79l-0.26,1c-0.11,0.37-0.27,0.84-0.48,1.41
138 + c-0.48,1.34-0.93,2.23-1.3,2.88c-2.89,5.53-8.81,12.35-20.71,14.15l-1.6,0.2c-0.92,0.11-1.76,0.17-2.55,0.18
139 + c-0.08,0-0.16-0.01-0.24-0.01l-0.26,0.01c-0.79-0.01-1.63-0.06-2.54-0.18l-1.5-0.18c-11.97-1.77-17.92-8.62-20.89-14.3
140 + c-0.29-0.51-0.74-1.41-1.24-2.8c-0.19-0.52-0.34-0.96-0.45-1.31l-0.27-1.05c-0.88-3.37-1.32-7.34-1.32-11.79V24.34H14.52v75.83
141 + c0.01,3.49,0.25,21.46,4.3,32.21c10.09,25.93,34.59,42.68,62.41,42.68h0.29c27.82,0,52.32-16.75,62.43-42.73
142 + c4.02-10.69,4.27-28.67,4.28-32.18V24.34H109.85z"/>
143 + </g>
144 + </g>
145 +</g>
146 +</svg>
added assets/uqo/UQO_BLANC.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_CMYK.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_BLANC.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_CMYK.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_NOIR.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_PMS7469.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_blanc.png +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_couleur.png +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_couleur_300dpi.jpg +0 −0

Binary file not shown.

added assets/uqo/UQO_Complet_noir.png +0 −0

Binary file not shown.

added assets/uqo/UQO_NOIR.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_PMS7469.eps +0 −0

Binary file not shown.

added assets/uqo/UQO_blanc.png +0 −0

Binary file not shown.

added assets/uqo/UQO_couleur.png +0 −0

Binary file not shown.

added assets/uqo/UQO_couleur_300dpi.jpg +0 −0

Binary file not shown.

added assets/uqo/logo_uqo_white.svg +1 −0
@@ -0,0 +1 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" id="Calque_1" data-name="Calque 1" viewBox="0 0 754.84 303.29"><defs><style>.cls-1{fill:#fff;stroke-width:0}</style></defs><path d="M291.33 108.99c10.66-35.7 48.37-56.08 84.07-45.42s56.08 48.37 45.42 84.07c-8.84 29.61-36.29 48.66-65.77 48.22l61.11 45.48c29.18-15.52 52.39-42.24 62.59-76.41 20.22-67.75-18.31-139.07-86.06-159.3C324.94-14.59 253.62 23.94 233.4 91.7c-15.95 53.43 4.66 109.06 47.31 140.12l71.58 53.28c28.18 21.74 49.62 18.9 57.96 16.74l46.89-14.07-141.32-105.19c-22.16-16.47-32.83-45.64-24.49-73.59ZM626.81 0C556.1 0 498.78 57.32 498.78 128.03s57.32 128.03 128.03 128.03 128.03-57.32 128.03-128.03C754.84 57.32 697.52 0 626.81 0Zm0 195.6c-37.26 0-67.57-30.31-67.57-67.57s30.31-67.57 67.57-67.57 67.57 30.31 67.57 67.57-30.31 67.57-67.57 67.57ZM151.81 16.22v128.54c0 7.09-.71 13.41-2.11 18.78l-.41 1.6c-.18.58-.43 1.34-.77 2.25-.77 2.14-1.48 3.56-2.07 4.59-4.6 8.81-14.03 19.67-32.98 22.53l-2.56.31c-1.46.18-2.8.27-4.07.28h-.79c-1.26 0-2.59-.09-4.05-.27l-2.38-.29c-19.07-2.82-28.54-13.73-33.27-22.77-.47-.82-1.18-2.24-1.98-4.46-.31-.83-.54-1.53-.71-2.09l-.43-1.66c-1.4-5.37-2.11-11.69-2.11-18.78V16.22H0v120.76c.01 5.56.4 34.17 6.85 51.31 16.07 41.29 55.08 67.97 99.39 67.97h.45c44.31 0 83.32-26.68 99.42-68.05 6.41-17.03 6.81-45.66 6.82-51.25V16.22h-61.12Z" class="cls-1"/></svg>
\ No newline at end of file
modified package-lock.json +5 −4
@@ -1,15 +1,15 @@
1 1 {
2 − "name": "vrai-prix",
2 + "name": "uqo-eval",
3 3 "version": "0.1.0",
4 4 "lockfileVersion": 3,
5 5 "requires": true,
6 6 "packages": {
7 7 "": {
8 − "name": "vrai-prix",
8 + "name": "uqo-eval",
9 9 "version": "0.1.0",
10 10 "dependencies": {
11 11 "@anthropic-ai/sdk": "^0.116.0",
12 − "@groupe-ka/ka-maps": "file:../../ka-maps/groupe-ka-ka-maps-0.1.0.tgz",
12 + "@groupe-ka/ka-maps": "file:./vendor/groupe-ka-ka-maps-0.1.0.tgz",
13 13 "better-sqlite3": "^13.0.3",
14 14 "mapbox-gl": "^3.28.1",
15 15 "next": "16.3.0",
@@ -494,7 +494,7 @@
494 494 },
495 495 "node_modules/@groupe-ka/ka-maps": {
496 496 "version": "0.1.0",
497 − "resolved": "file:../../ka-maps/groupe-ka-ka-maps-0.1.0.tgz",
497 + "resolved": "file:vendor/groupe-ka-ka-maps-0.1.0.tgz",
498 498 "integrity": "sha512-zXL7KpTGQ2qIhS+w2Eh3I50lIafLLtinQ1r4+6/aQD/W0Ch3SYYVTxch4LwsdOzVGZ+re1uKeKuMGpEF4HJTdw==",
499 499 "license": "UNLICENSED",
500 500 "dependencies": {
@@ -3250,6 +3250,7 @@
3250 3250 "version": "13.0.3",
3251 3251 "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
3252 3252 "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
3253 + "hasInstallScript": true,
3253 3254 "license": "MIT",
3254 3255 "dependencies": {
3255 3256 "node-addon-api": "^8.0.0"
modified package.json +3 −3
@@ -1,5 +1,5 @@
1 1 {
2 − "name": "vrai-prix",
2 + "name": "uqo-eval",
3 3 "version": "0.1.0",
4 4 "private": true,
5 5 "scripts": {
@@ -11,7 +11,7 @@
11 11 },
12 12 "dependencies": {
13 13 "@anthropic-ai/sdk": "^0.116.0",
14 − "@groupe-ka/ka-maps": "file:../../ka-maps/groupe-ka-ka-maps-0.1.0.tgz",
14 + "@groupe-ka/ka-maps": "file:./vendor/groupe-ka-ka-maps-0.1.0.tgz",
15 15 "better-sqlite3": "^13.0.3",
16 16 "mapbox-gl": "^3.28.1",
17 17 "next": "16.3.0",
@@ -33,4 +33,4 @@
33 33 "typescript": "^5",
34 34 "vitest": "^4.1.10"
35 35 }
36 −}
36 +}
\ No newline at end of file
modified public/apple-touch-icon.png +0 −0

Binary file not shown.

renamed public/ka-agent.js → public/assistant.js +29 −42
@@ -1,8 +1,8 @@
1 −/* Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 − * ka-ui/agent/ka-agent.js — KA AGENT : widget de chat commun de l'écosystème
3 − * Groupe KA. Vanilla JS, zéro dépendance, injecté sur les 13 sites.
4 − * <script>window.KA_AGENT={site:"lou-ka"}</script>
5 − * <script src="/ka-agent.js" defer></script>
1 +/* UQO Éval — Éva, l'assistante pédagogique (widget de chat).
2 + * Dérivé du widget ka-agent (vanilla JS, zéro dépendance), rebranché sur
3 + * l'API LOCALE /api/ka (agent Claude + outils du moteur d'évaluation).
4 + * <script>window.KA_AGENT={site:"uqo-eval"}</script>
5 + * <script src="/assistant.js" defer></script>
6 6 * v2 (2026-08-19) : ouverture directement PLEIN ÉCRAN (desktop et mobile,
7 7 * bouton ▭ pour réduire en fenêtre sur desktop) ; sur téléphone le panneau se
8 8 * cale sur window.visualViewport (le clavier ne fait plus grossir/défiler la
@@ -28,16 +28,16 @@
28 28 (function () {
29 29 "use strict";
30 30 var CFG = window.KA_AGENT || {};
31 − var SITE = CFG.site || "groupe-ka";
32 − var ENDPOINT = CFG.endpoint || "https://www.api-ka.com/api/agent/chat";
31 + var SITE = CFG.site || "uqo-eval";
32 + var ENDPOINT = CFG.endpoint || "/api/ka"; // API locale — aucune dépendance externe
33 33 var KEY = "ka-agent-" + SITE;
34 34 var MAXLOG = 20;
35 35
36 36 /* ---------- styles ---------- */
37 37 var css = `
38 38 .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px;
39 − border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);
40 − color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;
39 + border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#006085);
40 + color:var(--on-accent,#ffffff);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;
41 41 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;
42 42 justify-content:center;transition:transform .15s;touch-action:none}
43 43 .kaa-btn:hover{transform:translate(-2px,-2px)}
@@ -55,20 +55,20 @@
55 55 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);
56 56 color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))}
57 57 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}
58 − .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);
58 + .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#006085);color:var(--on-accent,#ffffff);
59 59 border-radius:6px;padding:1px 7px 2px;transform:rotate(-2deg);font-weight:700}
60 60 .kaa-head small{font:700 9.5px "JetBrains Mono",monospace;letter-spacing:.09em;text-transform:uppercase;opacity:.65}
61 61 .kaa-head .kaa-sp{flex:1}
62 62 .kaa-hbtn{width:34px;height:34px;border:1.5px solid rgba(245,243,238,.4);border-radius:8px;background:none;
63 63 color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1;flex:none}
64 − .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)}
64 + .kaa-hbtn:hover{border-color:var(--accent,#006085);color:var(--accent,#006085)}
65 65 .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;
66 66 overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
67 67 .kaa-msg{max-width:86%;padding:9px 13px;border:1.5px solid var(--ink,#141814);border-radius:12px;
68 68 font-size:13.5px;line-height:1.55;word-break:break-word}
69 69 .kaa-full .kaa-msg{max-width:min(86%,760px)}
70 70 .kaa-full .kaa-log{align-items:stretch}
71 − .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);
71 + .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#006085);color:var(--on-accent,#ffffff);
72 72 border-bottom-right-radius:4px;white-space:pre-wrap}
73 73 .kaa-msg.kaa-a{align-self:flex-start;background:#fff;border-bottom-left-radius:4px;box-shadow:3px 3px 0 rgba(20,24,20,.12)}
74 74 .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px;font-weight:600}
@@ -86,12 +86,12 @@
86 86 .kaa-msg.kaa-a pre{margin:.45em 0;padding:9px 11px;background:var(--ink,#141814);color:var(--paper,#f5f3ee);
87 87 border-radius:9px;overflow-x:auto}
88 88 .kaa-msg.kaa-a pre code{background:none;border:0;color:inherit;padding:0;font-weight:400;font-size:11.5px;line-height:1.5}
89 − .kaa-msg.kaa-a blockquote{margin:.45em 0;padding:.15em .8em;border-left:3px solid var(--accent,#d9f26b);
89 + .kaa-msg.kaa-a blockquote{margin:.45em 0;padding:.15em .8em;border-left:3px solid var(--accent,#006085);
90 90 background:var(--surface-2,#faf9f5);color:var(--ink-2,#4d5551)}
91 91 .kaa-msg.kaa-a hr{border:0;border-top:1.5px dashed rgba(20,24,20,.3);margin:.6em 0}
92 92 .kaa-msg.kaa-a table{border-collapse:collapse;margin:.45em 0;font-size:12.5px;display:block;overflow-x:auto;max-width:100%}
93 93 .kaa-msg.kaa-a th,.kaa-msg.kaa-a td{border:1.5px solid var(--ink,#141814);padding:4px 9px;text-align:left;white-space:nowrap}
94 − .kaa-msg.kaa-a th{background:var(--accent,#d9f26b);color:var(--on-accent,#141814);
94 + .kaa-msg.kaa-a th{background:var(--accent,#006085);color:var(--on-accent,#ffffff);
95 95 font:700 11px "Space Grotesk",system-ui,sans-serif;letter-spacing:.02em}
96 96 .kaa-msg.kaa-a td{background:#fff}
97 97 .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em;
@@ -104,12 +104,12 @@
104 104 .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:10px 12px;font:16px/1.4 Inter,system-ui,sans-serif;
105 105 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}
106 106 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;
107 − background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer;touch-action:manipulation}
107 + background:var(--ink,#141814);color:var(--accent,#006085);font:700 16px "Space Grotesk",system-ui;cursor:pointer;touch-action:manipulation}
108 108 .kaa-in button:disabled{opacity:.45}
109 109 .kaa-cards{display:flex;flex-direction:column;gap:8px;margin:.5em 0 .2em}
110 110 .kaa-card{display:flex;align-items:stretch;border:1.5px solid var(--ink,#141814);border-radius:11px;
111 111 overflow:hidden;background:#fff;text-decoration:none!important;color:var(--ink,#141814);
112 − box-shadow:3px 3px 0 rgba(20,24,20,.14);border-left:5px solid var(--kaa-c,var(--accent,#d9f26b));transition:transform .12s}
112 + box-shadow:3px 3px 0 rgba(20,24,20,.14);border-left:5px solid var(--kaa-c,var(--accent,#006085));transition:transform .12s}
113 113 .kaa-msg.kaa-a a.kaa-card{text-decoration:none;font-weight:400}
114 114 .kaa-card:hover{transform:translate(-1px,-1px)}
115 115 .kaa-card-img{flex:none;width:76px;min-height:76px;background-size:cover;background-position:center;
@@ -117,7 +117,7 @@
117 117 .kaa-card-body{flex:1;display:flex;flex-direction:column;gap:2px;padding:8px 11px;min-width:0}
118 118 .kaa-card-top{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
119 119 .kaa-card-site{font:700 9px "JetBrains Mono",monospace;letter-spacing:.08em;text-transform:uppercase;
120 − background:var(--kaa-c,var(--accent,#d9f26b));color:var(--kaa-on,var(--on-accent,#141814));border-radius:5px;padding:1px 6px 2px}
120 + background:var(--kaa-c,var(--accent,#006085));color:var(--kaa-on,var(--on-accent,#ffffff));border-radius:5px;padding:1px 6px 2px}
121 121 .kaa-card-badge{font:700 9px "JetBrains Mono",monospace;letter-spacing:.06em;text-transform:uppercase;
122 122 border:1px dashed var(--ink-2,#4d5551);border-radius:999px;padding:1px 7px;color:var(--ink-2,#4d5551)}
123 123 .kaa-card-title{font:700 13.5px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.01em;line-height:1.25}
@@ -129,7 +129,7 @@
129 129 .kaa-choice{border:1.5px solid var(--ink,#141814);border-radius:999px;background:var(--paper,#f5f3ee);
130 130 color:var(--ink,#141814);font:600 12.5px Inter,system-ui,sans-serif;padding:7px 13px;cursor:pointer;
131 131 box-shadow:2px 2px 0 rgba(20,24,20,.35);transition:transform .1s;touch-action:manipulation}
132 − .kaa-choice:hover{background:var(--accent,#d9f26b);color:var(--on-accent,#141814);transform:translate(-1px,-1px)}
132 + .kaa-choice:hover{background:var(--accent,#006085);color:var(--on-accent,#ffffff);transform:translate(-1px,-1px)}
133 133 .kaa-choices.kaa-done .kaa-choice{opacity:.45;pointer-events:none;box-shadow:none}
134 134 .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px;max-width:760px}
135 135 .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite}
@@ -146,20 +146,7 @@
146 146 Palette de l'écosystème (ka-ui/ecosystem.json) embarquée : chaque carte
147 147 prend les couleurs de la plateforme de SA fiche, pas du site hôte. */
148 148 var ECO = {
149 − "groupe-ka": ["Groupe KA", "#d9f26b", "#141814"],
150 − "trouve-ka": ["Trouve·Ka", "#1c7ed6", "#ffffff"],
151 − "lou-ka": ["Lou·Ka", "#ff6a00", "#ffffff"],
152 − "immo-ka": ["Immo·Ka", "#e23744", "#ffffff"],
153 − "vrai-prix": ["Vrai-Prix", "#ff5148", "#ffffff"],
154 − "auto-ka": ["Auto·Ka", "#ff5a2a", "#ffffff"],
155 − "fabri-ka": ["Fabri·Ka", "#c4532e", "#ffffff"],
156 − "food-ka": ["Food·Ka", "#1f9d55", "#ffffff"],
157 − "resto-ka": ["Resto·Ka", "#f08c00", "#141814"],
158 − "sorti-ka": ["Sorti·Ka", "#d6336c", "#ffffff"],
159 − "crea-ka": ["Créa·Ka", "#7048e8", "#ffffff"],
160 − "api-ka": ["API·Ka", "#3b5bdb", "#ffffff"],
161 − "job-ka": ["Job·Ka", "#0c8599", "#ffffff"],
162 − "valoplex": ["ValoPlex", "#ff9f45", "#141814"]
149 + "uqo-eval": ["UQO Éval", "#006085", "#ffffff"],
163 150 };
164 151 function httpUrl(u) { return typeof u === "string" && /^https:\/\//.test(u) ? u : ""; }
165 152 /* Extrait chaque objet JSON de premier niveau d'un bloc ka-card, quelle que
@@ -191,7 +178,7 @@
191 178 var html = "";
192 179 jsonObjects(lines.join("\n")).forEach(function (o) {
193 180 if (!o || !o.titre || !httpUrl(o.lien)) return;
194 − var pal = ECO[o.site] || ["Groupe KA", "var(--accent,#d9f26b)", "var(--on-accent,#141814)"];
181 + var pal = ECO[o.site] || ["UQO Éval", "var(--accent,#006085)", "var(--on-accent,#ffffff)"];
195 182 var img = httpUrl(o.image);
196 183 html += '<a class="kaa-card" href="' + esc(o.lien) + '" target="_blank" rel="noopener noreferrer"' +
197 184 ' style="--kaa-c:' + pal[1] + ";--kaa-on:" + pal[2] + '">' +
@@ -301,15 +288,15 @@
301 288
302 289 var btn = document.createElement("button");
303 290 btn.className = "kaa-btn";
304 − btn.setAttribute("aria-label", "Ouvrir KA Agent — assistant de l'écosystème Groupe KA");
305 − btn.innerHTML = "Ka";
291 + btn.setAttribute("aria-label", "Ouvrir Éva — l'assistante pédagogique d'UQO Éval");
292 + btn.innerHTML = "Éva";
306 293
307 294 var panel = document.createElement("div");
308 295 panel.className = "kaa-panel";
309 296 panel.setAttribute("role", "dialog");
310 − panel.setAttribute("aria-label", "KA Agent");
297 + panel.setAttribute("aria-label", "Éva — assistante pédagogique");
311 298 panel.innerHTML =
312 − '<div class="kaa-head"><b>KA<span class="kaa-ka">Agent</span></b><small>Groupe KA · IA</small><span class="kaa-sp"></span>' +
299 + '<div class="kaa-head"><b>UQO<span class="kaa-ka">Éval</span></b><small>Éva · assistante pédagogique</small><span class="kaa-sp"></span>' +
313 300 '<button class="kaa-hbtn kaa-x1" title="Réduire / agrandir" aria-label="Réduire ou agrandir">▭</button>' +
314 301 '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' +
315 302 '<div class="kaa-log"></div>' +
@@ -344,7 +331,7 @@
344 331 if (log.length) return;
345 332 var d = document.createElement("div");
346 333 d.className = "kaa-hello";
347 − d.textContent = "👋 Je suis KA Agent, l'assistant de l'écosystème Groupe KA. Je peux chercher dans TOUTES les plateformes du groupe : logements, propriétés, autos, emplois, épicerie, restos, sorties, créateurs, produits d'ici — plus estimations de valeur, annuaires (déménageurs, inspecteurs), historiques et statistiques. Posez votre question !";
334 + d.textContent = "👋 Je suis Éva, l'assistante pédagogique d'UQO Éval. Donnez-moi une adresse du Québec : je la retrouve dans le registre, je l'évalue avec le moteur transparent de la plateforme et je vous explique le calcul — comparables, ajustements, marché local, indice de confiance. Posez votre question !";
348 335 logEl.appendChild(d);
349 336 }
350 337 /* les boutons de choix ne restent actifs que dans la DERNIÈRE bulle assistante */
@@ -534,11 +521,12 @@
534 521 var blocks = buf.split("\n\n");
535 522 buf = blocks.pop();
536 523 blocks.forEach(function (b) {
537 − var ev = (b.match(/^event: (.+)$/m) || [])[1];
538 524 var dm = (b.match(/^data: (.+)$/m) || [])[1];
539 − if (!ev || !dm) return;
525 + if (!dm) return;
540 526 var data = {};
541 527 try { data = JSON.parse(dm); } catch (e) {}
528 + var ev = (b.match(/^event: (.+)$/m) || [])[1] ||
529 + (data.type === "text" ? "delta" : data.type); // format local /api/ka
542 530 if (ev === "delta" && data.text) {
543 531 acc += data.text;
544 532 out.innerHTML = md(acc); // markdown re-rendu à chaque delta
@@ -558,7 +546,7 @@
558 546 return pump();
559 547 })
560 548 .catch(function (e) {
561 − acc = acc || "Impossible de joindre KA Agent (" + e.message + "). Réessayez dans un instant.";
549 + acc = acc || "Impossible de joindre Éva (" + e.message + "). Réessayez dans un instant.";
562 550 out.innerHTML = md(acc);
563 551 })
564 552 .then(function () { finish(); });
@@ -587,4 +575,3 @@
587 575 ask();
588 576 });
589 577 })();
590 −/*KA_ANALYTICS*/(function(){try{if(window.__kaAn)return;window.__kaAn=1;var k="KA_VID",v=localStorage.getItem(k);if(!v){v=Date.now().toString(36)+Math.random().toString(36).slice(2,8);localStorage.setItem(k,v);}var b=function(){var i=new Image();i.src="https://www.administration-ka.com/collect?site=vrai-prix&vid="+v+"&p="+encodeURIComponent(location.pathname)+"&t="+Date.now();};b();setInterval(b,60000);}catch(e){}})();
deleted public/doc/img/etape1.png +0 −0

Binary file not shown.

deleted public/doc/img/etape2.png +0 −0

Binary file not shown.

deleted public/doc/img/etape3.png +0 −0

Binary file not shown.

deleted public/doc/img/etape4.png +0 −0

Binary file not shown.

deleted public/doc/img/etape5.png +0 −0

Binary file not shown.

deleted public/doc/index.html +0 −198
@@ -1,198 +0,0 @@
1 −<!DOCTYPE html>
2 −<!-- Page /doc de Vrai-Prix — générée depuis le gabarit commun Groupe KA (2026-08-24). -->
3 −<html lang="fr">
4 −<head>
5 −<meta charset="utf-8">
6 −<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
7 −<title>Documentation — Vrai-Prix</title>
8 −<meta name="description" content="Comment fonctionne Vrai-Prix : guide pas à pas, données, architecture.">
9 −<style>
10 − :root{ --ink:#101014; --paper:#faf9f6; --accent:#ff5148; --muted:#6b6b70; --line:#e6e4de; }
11 − *{box-sizing:border-box} html,body{margin:0;padding:0;background:var(--paper);color:var(--ink);
12 − font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
13 − .container{max-width:960px;margin:0 auto;padding:0 20px}
14 − header.doc{background:var(--ink);color:var(--paper);padding:20px 0}
15 − header.doc .container{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
16 − header.doc a{color:var(--paper);text-decoration:none}
17 − .wordmark{font-weight:800;font-size:20px;letter-spacing:.02em}
18 − .wordmark .ka{color:var(--accent)}
19 − .btn-pdf{display:inline-block;background:var(--accent);color:var(--ink);font-weight:700;
20 − padding:10px 18px;border-radius:999px;text-decoration:none;font-size:15px}
21 − .hero{padding:48px 0 8px}
22 − .hero h1{font-size:clamp(28px,5vw,44px);line-height:1.1;margin:0 0 12px}
23 − .hero h1 mark{background:var(--accent);color:var(--ink);padding:2px 8px}
24 − .hero p.lead{font-size:18px;color:var(--muted);max-width:640px}
25 − .kicker{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
26 − border-left:3px solid var(--accent);padding-left:10px;margin:40px 0 8px;font-weight:700}
27 − section{padding:8px 0 16px}
28 − h2{font-size:26px;margin:6px 0 12px}
29 − .step{display:grid;grid-template-columns:56px 1fr;gap:16px;margin:26px 0;align-items:start}
30 − .step .num{width:44px;height:44px;border-radius:50%;background:var(--ink);color:var(--accent);
31 − display:flex;align-items:center;justify-content:center;font-weight:800;font-size:18px}
32 − .step h3{margin:6px 0 6px;font-size:19px}
33 − .step p{margin:0 0 12px;color:#3a3a40}
34 − .shot{border:1px solid var(--line);border-radius:12px;overflow:hidden;box-shadow:0 8px 30px rgba(16,16,20,.08)}
35 − .shot img{display:block;width:100%;height:auto}
36 − .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin:16px 0}
37 − .card{border:1px solid var(--line);border-radius:12px;padding:16px;background:#fff}
38 − .card b{display:block;margin-bottom:6px}
39 − table{border-collapse:collapse;width:100%;font-size:15px}
40 − th,td{border:1px solid var(--line);padding:8px 10px;text-align:left;vertical-align:top}
41 − th{background:var(--ink);color:var(--paper)}
42 − footer.doc{margin-top:56px;background:var(--ink);color:var(--paper);padding:28px 0;font-size:14px}
43 − footer.doc a{color:var(--accent);text-decoration:none}
44 − @media print{
45 − header.doc .btn-pdf{display:none}
46 − .shot{box-shadow:none;break-inside:avoid}
47 − .step{break-inside:avoid}
48 − a{color:inherit;text-decoration:none}
49 − }
50 − @media(max-width:640px){ .step{grid-template-columns:40px 1fr} .step .num{width:34px;height:34px;font-size:15px} }
51 −</style>
52 −</head>
53 −<body>
54 −<header class="doc">
55 − <div class="container">
56 − <a class="wordmark" href="/">Vrai-<span class="ka">Prix</span></a>
57 − <nav style="display:flex;gap:14px;align-items:center">
58 − <a href="/">← Retour au site</a>
59 − <a class="btn-pdf" href="./vrai-prix-documentation.pdf" download>Télécharger le PDF</a>
60 − </nav>
61 − </div>
62 −</header>
63 −
64 −<div class="container">
65 − <div class="hero">
66 − <p class="kicker">Documentation · mise à jour 2026-08-24</p>
67 − <h1>Comment fonctionne <mark>Vrai-Prix</mark></h1>
68 − <p class="lead">La vraie valeur de votre propriété, sans boîte noire — estimation immobilière transparente pour tout le Québec.</p>
69 − </div>
70 −
71 − <section>
72 − <p class="kicker">Vue d'ensemble</p>
73 − <h2>À quoi sert le site</h2>
74 − <p>Vrai-Prix estime la valeur marchande de n'importe quelle propriété du Québec — unifamiliale, plex, condo,
75 − chalet, terrain — et montre <em>pourquoi</em> : chaque comparable, chaque ajustement en dollars, la pondération
76 − du modèle, la fourchette honnête (P10–P90) et l'indice de confiance (A–D). Pas de score magique ni de boîte
77 − noire : la méthodologie complète (normes IAAO) est publiée intégralement sur le site. Pour les propriétaires,
78 − acheteurs, courtiers et curieux qui veulent une estimation honnête, vérifiable et gratuite, dans les
79 − 1 097 municipalités de la province.</p>
80 − <div class="cards">
81 − <div class="card"><b>3 747 008 propriétés</b>Les 6 millésimes (2021-2026) des rôles d'évaluation foncière
82 − géoréférencés du Québec (MAMH, données ouvertes) — 22 150 285 observations.</div>
83 − <div class="card"><b>745 119 ventes réelles</b>Transactions fusionnées spatialement avec les rôles
84 − (99,88 % d'appariement, distance médiane 0,6 m).</div>
85 − <div class="card"><b>MdAPE 11,0 %</b>Modèle hédonique LightGBM (ratio médian 0,995 en holdout) combiné
86 − à un moteur de comparables ajustés en dollars.</div>
87 − <div class="card"><b>1 097 municipalités</b>Couverture provinciale complète, valeur totale du parc :
88 − 2,01 billions $ — statistiques et palmarès publics.</div>
89 − </div>
90 − </section>
91 −
92 − <section>
93 − <p class="kicker">Guide pas à pas</p>
94 − <h2>Utiliser le site en 5 étapes</h2>
95 −
96 − <div class="step">
97 − <div class="num">1</div>
98 − <div>
99 − <h3>Arriver sur l'accueil</h3>
100 − <p>La page d'accueil annonce la couleur : 3,7 M de propriétés, 745 000 ventes réelles, fourchette honnête
101 − P10–P90, indice de confiance A–D. La barre « Adresse de la propriété » au centre est le point de départ ;
102 − si vous n'avez pas d'adresse, un plan B permet d'estimer manuellement par municipalité et caractéristiques.</p>
103 − <div class="shot"><img src="./img/etape1.png" alt="Étape 1 — accueil de Vrai-Prix"></div>
104 − </div>
105 − </div>
106 −
107 − <div class="step">
108 − <div class="num">2</div>
109 − <div>
110 − <h3>Chercher l'adresse</h3>
111 − <p>Tapez l'adresse dans la barre de recherche : la recherche plein-texte (tolérante aux fautes) fouille les
112 − 3 747 008 unités d'évaluation et propose les correspondances au fil de la frappe. Cliquez sur la bonne
113 − adresse dans la liste.</p>
114 − <div class="shot"><img src="./img/etape2.png" alt="Étape 2 — recherche d'adresse avec suggestions"></div>
115 − </div>
116 − </div>
117 −
118 − <div class="step">
119 − <div class="num">3</div>
120 − <div>
121 − <h3>Lire la fiche d'estimation</h3>
122 − <p>La fiche montre la valeur estimée, la fourchette honnête P10–P90, l'indice de confiance (A–D), l'écart
123 − avec l'évaluation municipale, puis tout le calcul : portrait de la propriété dessiné par les données du
124 − registre, comparables ajustés en dollars, historique 2021→2026. Deux rapports PDF sont téléchargeables :
125 − standard (3 p.) et professionnel format bancaire (6 p.).</p>
126 − <div class="shot"><img src="./img/etape3.png" alt="Étape 3 — fiche d'estimation avec fourchette et indice de confiance"></div>
127 − </div>
128 − </div>
129 −
130 − <div class="step">
131 − <div class="num">4</div>
132 − <div>
133 − <h3>Explorer par la carte</h3>
134 − <p>L'onglet « Carte » ouvre Vrai-Prix Maps : déplacez la carte 3D ou géolocalisez-vous — chaque pastille est
135 − une unité d'évaluation avec sa valeur estimée 2026. Touchez une pastille pour l'aperçu, puis passez à
136 − l'estimation complète.</p>
137 − <div class="shot"><img src="./img/etape4.png" alt="Étape 4 — recherche par carte 3D"></div>
138 − </div>
139 − </div>
140 −
141 − <div class="step">
142 − <div class="num">5</div>
143 − <div>
144 − <h3>Aller plus loin : statistiques, parc, agent Ka</h3>
145 − <p>La page « Statistiques » donne le portrait provincial (valeur du parc, palmarès des 200 municipalités,
146 − rapports PDF). « Parc immobilier » agrège jusqu'à 40 adresses en une évaluation consolidée, et
147 − « Ka · agent IA » répond en langage naturel, branché sur 12 outils serveur (évaluation, comparables,
148 − stats, dossier d'expert).</p>
149 − <div class="shot"><img src="./img/etape5.png" alt="Étape 5 — tableau de bord statistiques"></div>
150 − </div>
151 − </div>
152 − </section>
153 −
154 − <section>
155 − <p class="kicker">Sous le capot</p>
156 − <h2>D'où viennent les données</h2>
157 − <p>Tout part des <b>rôles d'évaluation foncière géoréférencés du Québec</b> (MAMH, données ouvertes) : six
158 − millésimes 2021-2026, soit 22,15 M d'observations sur 3,75 M d'unités d'évaluation. Ces rôles sont fusionnés
159 − spatialement (au mètre près — distance médiane 0,6 m) avec <b>745 119 transactions réelles</b>. Le tout
160 − alimente un <b>modèle hédonique LightGBM</b> (log-prix, 18 variables, quantiles P10/P90) réentraîné par
161 − pipeline externe, et un <b>moteur de comparables</b> qui ajuste chaque vente voisine en dollars (marché,
162 − superficie, âge). L'estimation finale est hybride : 65 % modèle + 35 % comparables, avec indice de confiance
163 − A–D obligatoire. Les salaires du calcul sont montrés en entier : c'est le principe « zéro boîte noire »,
164 − conforme aux normes IAAO documentées dans la méthodologie publique du site.</p>
165 − </section>
166 −
167 − <section>
168 − <p class="kicker">Questions fréquentes</p>
169 − <h2>FAQ</h2>
170 − <div class="cards">
171 − <div class="card"><b>L'estimation remplace-t-elle un évaluateur agréé ?</b>
172 − Non. C'est une estimation statistique à titre indicatif — elle ne remplace pas une évaluation par un
173 − évaluateur agréé (OEAQ). Sa force : la transparence et la fourchette honnête plutôt qu'un chiffre unique.</div>
174 − <div class="card"><b>Mon adresse n'apparaît pas dans la recherche, que faire ?</b>
175 − Utilisez le plan B sans adresse : estimation manuelle par municipalité + caractéristiques (superficie, année,
176 − terrain). La recherche est tolérante aux fautes — essayez aussi une graphie simplifiée.</div>
177 − <div class="card"><b>Que signifie l'indice de confiance A–D ?</b>
178 − Il mesure la fiabilité de l'estimation selon la densité de ventes comparables et la qualité des données
179 − locales : A = très fiable, D = à prendre avec prudence. Il est affiché sur chaque fiche, sans exception.</div>
180 − <div class="card"><b>Est-ce gratuit ? Faut-il un compte ?</b>
181 − L'estimation, la carte, les statistiques et les rapports PDF sont gratuits, sans compte. KA ID (la connexion
182 − unique du Groupe KA) sert seulement aux favoris « Mon univers Ka ».</div>
183 − <div class="card"><b>Le site est-il disponible en anglais ?</b>
184 − Oui, le site est bilingue FR/EN (bouton EN dans l'en-tête), et la méthodologie complète en 7 sections est
185 − publique.</div>
186 − </div>
187 − </section>
188 −</div>
189 −
190 −<footer class="doc">
191 − <div class="container">
192 − <p><b>Vrai-Prix</b> — un service <a href="https://www.groupe-ka.com">Groupe KA</a>.
193 − Écosystème : les 13 plateformes sont liées au pied de chaque site.</p>
194 − <p>© 2026 Groupe KA — Simon-Pierre Boucher · <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a></p>
195 − </div>
196 −</footer>
197 −</body>
198 −</html>
deleted public/doc/vrai-prix-documentation.pdf +0 −0

Binary file not shown.

modified public/favicon.svg +4 −4
@@ -1,5 +1,5 @@
1 −<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 −<rect width="64" height="64" rx="14" fill="#141814"/>
3 −<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#ff5148" stroke-opacity="0.35" stroke-width="2"/>
4 −<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#ff5148" transform="rotate(-4 32 32)">Ka</text>
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2 + <rect width="64" height="64" rx="12" fill="#006085"/>
3 + <image href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJDYWxxdWVfMSIgZGF0YS1uYW1lPSJDYWxxdWUgMSIgdmlld0JveD0iMCAwIDc1NC44NCAzMDMuMjkiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojZmZmO3N0cm9rZS13aWR0aDowfTwvc3R5bGU+PC9kZWZzPjxwYXRoIGQ9Ik0yOTEuMzMgMTA4Ljk5YzEwLjY2LTM1LjcgNDguMzctNTYuMDggODQuMDctNDUuNDJzNTYuMDggNDguMzcgNDUuNDIgODQuMDdjLTguODQgMjkuNjEtMzYuMjkgNDguNjYtNjUuNzcgNDguMjJsNjEuMTEgNDUuNDhjMjkuMTgtMTUuNTIgNTIuMzktNDIuMjQgNjIuNTktNzYuNDEgMjAuMjItNjcuNzUtMTguMzEtMTM5LjA3LTg2LjA2LTE1OS4zQzMyNC45NC0xNC41OSAyNTMuNjIgMjMuOTQgMjMzLjQgOTEuN2MtMTUuOTUgNTMuNDMgNC42NiAxMDkuMDYgNDcuMzEgMTQwLjEybDcxLjU4IDUzLjI4YzI4LjE4IDIxLjc0IDQ5LjYyIDE4LjkgNTcuOTYgMTYuNzRsNDYuODktMTQuMDctMTQxLjMyLTEwNS4xOWMtMjIuMTYtMTYuNDctMzIuODMtNDUuNjQtMjQuNDktNzMuNTlaTTYyNi44MSAwQzU1Ni4xIDAgNDk4Ljc4IDU3LjMyIDQ5OC43OCAxMjguMDNzNTcuMzIgMTI4LjAzIDEyOC4wMyAxMjguMDMgMTI4LjAzLTU3LjMyIDEyOC4wMy0xMjguMDNDNzU0Ljg0IDU3LjMyIDY5Ny41MiAwIDYyNi44MSAwWm0wIDE5NS42Yy0zNy4yNiAwLTY3LjU3LTMwLjMxLTY3LjU3LTY3LjU3czMwLjMxLTY3LjU3IDY3LjU3LTY3LjU3IDY3LjU3IDMwLjMxIDY3LjU3IDY3LjU3LTMwLjMxIDY3LjU3LTY3LjU3IDY3LjU3Wk0xNTEuODEgMTYuMjJ2MTI4LjU0YzAgNy4wOS0uNzEgMTMuNDEtMi4xMSAxOC43OGwtLjQxIDEuNmMtLjE4LjU4LS40MyAxLjM0LS43NyAyLjI1LS43NyAyLjE0LTEuNDggMy41Ni0yLjA3IDQuNTktNC42IDguODEtMTQuMDMgMTkuNjctMzIuOTggMjIuNTNsLTIuNTYuMzFjLTEuNDYuMTgtMi44LjI3LTQuMDcuMjhoLS43OWMtMS4yNiAwLTIuNTktLjA5LTQuMDUtLjI3bC0yLjM4LS4yOWMtMTkuMDctMi44Mi0yOC41NC0xMy43My0zMy4yNy0yMi43Ny0uNDctLjgyLTEuMTgtMi4yNC0xLjk4LTQuNDYtLjMxLS44My0uNTQtMS41My0uNzEtMi4wOWwtLjQzLTEuNjZjLTEuNC01LjM3LTIuMTEtMTEuNjktMi4xMS0xOC43OFYxNi4yMkgwdjEyMC43NmMuMDEgNS41Ni40IDM0LjE3IDYuODUgNTEuMzEgMTYuMDcgNDEuMjkgNTUuMDggNjcuOTcgOTkuMzkgNjcuOTdoLjQ1YzQ0LjMxIDAgODMuMzItMjYuNjggOTkuNDItNjguMDUgNi40MS0xNy4wMyA2LjgxLTQ1LjY2IDYuODItNTEuMjVWMTYuMjJoLTYxLjEyWiIgY2xhc3M9ImNscy0xIi8+PC9zdmc+" x="7" y="19" width="50" height="20.1"/>
4 + <rect x="7" y="44" width="22" height="4" fill="#2BCCAE"/>
5 5 </svg>
\ No newline at end of file
deleted public/file.svg +0 −1
@@ -1 +0,0 @@
1 −<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
\ No newline at end of file
deleted public/globe.svg +0 −1
@@ -1 +0,0 @@
1 −<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
\ No newline at end of file
added public/manifest.webmanifest +14 −0
@@ -0,0 +1,14 @@
1 +{
2 + "name": "UQO Éval — Laboratoire pédagogique d'évaluation immobilière",
3 + "short_name": "UQO Éval",
4 + "description": "Comprendre comment une propriété est évaluée : modèle hédonique, comparables réels, intervalle de confiance — le calcul montré en entier.",
5 + "start_url": "/",
6 + "display": "standalone",
7 + "background_color": "#f4f5f3",
8 + "theme_color": "#006085",
9 + "lang": "fr-CA",
10 + "icons": [
11 + { "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" },
12 + { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml" }
13 + ]
14 +}
deleted public/next.svg +0 −1
@@ -1 +0,0 @@
1 −<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
modified public/og.png +0 −0

Binary file not shown.

added public/uqo/uqo-blanc.svg +1 −0
@@ -0,0 +1 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" id="Calque_1" data-name="Calque 1" viewBox="0 0 754.84 303.29"><defs><style>.cls-1{fill:#fff;stroke-width:0}</style></defs><path d="M291.33 108.99c10.66-35.7 48.37-56.08 84.07-45.42s56.08 48.37 45.42 84.07c-8.84 29.61-36.29 48.66-65.77 48.22l61.11 45.48c29.18-15.52 52.39-42.24 62.59-76.41 20.22-67.75-18.31-139.07-86.06-159.3C324.94-14.59 253.62 23.94 233.4 91.7c-15.95 53.43 4.66 109.06 47.31 140.12l71.58 53.28c28.18 21.74 49.62 18.9 57.96 16.74l46.89-14.07-141.32-105.19c-22.16-16.47-32.83-45.64-24.49-73.59ZM626.81 0C556.1 0 498.78 57.32 498.78 128.03s57.32 128.03 128.03 128.03 128.03-57.32 128.03-128.03C754.84 57.32 697.52 0 626.81 0Zm0 195.6c-37.26 0-67.57-30.31-67.57-67.57s30.31-67.57 67.57-67.57 67.57 30.31 67.57 67.57-30.31 67.57-67.57 67.57ZM151.81 16.22v128.54c0 7.09-.71 13.41-2.11 18.78l-.41 1.6c-.18.58-.43 1.34-.77 2.25-.77 2.14-1.48 3.56-2.07 4.59-4.6 8.81-14.03 19.67-32.98 22.53l-2.56.31c-1.46.18-2.8.27-4.07.28h-.79c-1.26 0-2.59-.09-4.05-.27l-2.38-.29c-19.07-2.82-28.54-13.73-33.27-22.77-.47-.82-1.18-2.24-1.98-4.46-.31-.83-.54-1.53-.71-2.09l-.43-1.66c-1.4-5.37-2.11-11.69-2.11-18.78V16.22H0v120.76c.01 5.56.4 34.17 6.85 51.31 16.07 41.29 55.08 67.97 99.39 67.97h.45c44.31 0 83.32-26.68 99.42-68.05 6.41-17.03 6.81-45.66 6.82-51.25V16.22h-61.12Z" class="cls-1"/></svg>
\ No newline at end of file
added public/uqo/uqo-bleu.png +0 −0

Binary file not shown.

added public/uqo/uqo-complet-blanc.svg +146 −0
@@ -0,0 +1,146 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<!-- Generator: Adobe Illustrator 27.8.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3 +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4 + viewBox="0 0 958.67 216" style="enable-background:new 0 0 958.67 216;" xml:space="preserve">
5 +<style type="text/css">
6 + .st0{fill:#FFFFFF;}
7 +</style>
8 +<g>
9 + <g>
10 + <path class="st0" d="M569,60.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29s-2.23-3.88-2.82-6.2
11 + c-0.6-2.32-0.9-4.73-0.9-7.23V21.19h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
12 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
13 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06V21.19h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
14 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.33,1.76-3.04,3.14-5.14,4.16C574.56,59.54,572,60.05,569,60.05z"/>
15 + <path class="st0" d="M599.87,32.49v27.28h-6.08V21.19h5l21.85,27.88V21.24h6.14v38.53h-5.32L599.87,32.49z"/>
16 + <path class="st0" d="M635.47,59.78V21.19h6.08v38.59H635.47z"/>
17 + <path class="st0" d="M652.69,21.19l11.63,31.14l11.58-31.14h6.47l-15.16,38.59h-5.76l-15.22-38.59H652.69z"/>
18 + <path class="st0" d="M713.56,54.4v5.38H687.1V21.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23V54.4H713.56z"/>
19 + <path class="st0" d="M719.87,59.78V21.19h16.79c1.74,0,3.34,0.36,4.78,1.09c1.45,0.72,2.71,1.69,3.78,2.88
20 + c1.07,1.19,1.9,2.54,2.5,4.02c0.6,1.49,0.89,3.01,0.89,4.57c0,1.7-0.31,3.34-0.92,4.89c-0.62,1.56-1.49,2.9-2.64,4.02
21 + c-1.14,1.12-2.47,1.94-4,2.45l9.08,14.67h-6.85l-8.37-13.42h-8.97v13.42H719.87z M725.95,40.97h10.65c1.12,0,2.12-0.33,2.99-0.98
22 + c0.87-0.65,1.56-1.53,2.07-2.64c0.51-1.11,0.76-2.31,0.76-3.61c0-1.34-0.29-2.54-0.87-3.61c-0.58-1.07-1.33-1.93-2.26-2.58
23 + c-0.92-0.65-1.93-0.98-3.01-0.98h-10.33V40.97z"/>
24 + <path class="st0" d="M778.62,30.27c-0.36-0.4-0.89-0.82-1.58-1.28c-0.69-0.45-1.51-0.88-2.45-1.28c-0.94-0.4-1.96-0.72-3.05-0.98
25 + c-1.08-0.25-2.21-0.38-3.37-0.38c-2.54,0-4.42,0.47-5.65,1.41c-1.23,0.94-1.85,2.24-1.85,3.9c0,1.23,0.34,2.19,1.03,2.9
26 + c0.69,0.71,1.74,1.29,3.15,1.76c1.41,0.47,3.19,0.98,5.33,1.52c2.61,0.61,4.87,1.35,6.79,2.19c1.92,0.85,3.39,1.98,4.4,3.38
27 + c1.01,1.41,1.52,3.27,1.52,5.58c0,1.95-0.36,3.63-1.09,5.04c-0.72,1.41-1.74,2.55-3.04,3.44c-1.3,0.88-2.81,1.53-4.51,1.95
28 + c-1.7,0.41-3.53,0.62-5.49,0.62c-1.92,0-3.83-0.2-5.73-0.6c-1.9-0.4-3.71-0.99-5.43-1.77c-1.72-0.78-3.31-1.73-4.76-2.85
29 + l2.77-5.22c0.47,0.47,1.15,0.99,2.04,1.55s1.92,1.1,3.1,1.6c1.18,0.51,2.46,0.93,3.86,1.28c1.4,0.35,2.82,0.52,4.26,0.52
30 + c2.43,0,4.29-0.42,5.57-1.27c1.29-0.84,1.93-2.06,1.93-3.65c0-1.3-0.42-2.34-1.25-3.12c-0.83-0.78-2.05-1.44-3.64-1.98
31 + c-1.6-0.54-3.5-1.08-5.71-1.63c-2.54-0.69-4.66-1.43-6.36-2.25c-1.7-0.81-2.98-1.86-3.83-3.14c-0.85-1.28-1.28-2.92-1.28-4.91
32 + c0-2.53,0.61-4.67,1.85-6.42c1.23-1.75,2.91-3.07,5.03-3.96c2.12-0.88,4.5-1.33,7.15-1.33c1.74,0,3.38,0.19,4.94,0.57
33 + c1.56,0.38,3.02,0.9,4.38,1.55c1.36,0.65,2.6,1.4,3.72,2.23L778.62,30.27z"/>
34 + <path class="st0" d="M789,59.78V21.19h6.08v38.59H789z"/>
35 + <path class="st0" d="M832.09,26.57h-12.83v33.21h-6.14V26.57h-12.88v-5.38h31.85V26.57z"/>
36 + <path class="st0" d="M863.72,54.4v5.38h-26.46V21.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23V54.4H863.72z M852.91,17.17
37 + l-3.91-1.2l3.1-5.97h5.71L852.91,17.17z"/>
38 + <path class="st0" d="M552.91,112.78V74.19h13.7c4.17,0,7.64,0.86,10.43,2.58c2.79,1.72,4.88,4.04,6.28,6.96
39 + c1.4,2.91,2.1,6.15,2.1,9.7c0,3.91-0.77,7.32-2.31,10.22s-3.72,5.14-6.55,6.74c-2.82,1.6-6.14,2.39-9.94,2.39H552.91z
40 + M579.22,93.43c0-2.72-0.5-5.12-1.49-7.2c-1-2.08-2.43-3.71-4.29-4.89c-1.87-1.18-4.14-1.77-6.82-1.77H559v27.82h7.61
41 + c2.72,0,5.01-0.61,6.87-1.85c1.87-1.23,3.29-2.9,4.27-5C578.73,98.45,579.22,96.08,579.22,93.43z"/>
42 + <path class="st0" d="M607.91,113.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29s-2.23-3.88-2.82-6.2
43 + c-0.6-2.32-0.9-4.73-0.9-7.23V74.19h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
44 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
45 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06V74.19h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
46 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.33,1.76-3.04,3.14-5.14,4.16C613.47,112.54,610.92,113.05,607.91,113.05z"/>
47 + <path class="st0" d="M662.15,113.05c-2.79,0-5.31-0.55-7.55-1.66c-2.25-1.11-4.18-2.58-5.82-4.43c-1.63-1.85-2.89-3.94-3.78-6.28
48 + c-0.89-2.34-1.33-4.74-1.33-7.2c0-2.57,0.47-5.03,1.41-7.37c0.94-2.34,2.25-4.42,3.91-6.25c1.66-1.83,3.63-3.28,5.9-4.35
49 + c2.26-1.07,4.72-1.6,7.36-1.6c2.75,0,5.26,0.56,7.53,1.69c2.26,1.12,4.2,2.63,5.82,4.51s2.86,3.99,3.75,6.3
50 + c0.89,2.32,1.33,4.69,1.33,7.12c0,2.57-0.46,5.03-1.39,7.36c-0.93,2.34-2.22,4.42-3.89,6.25c-1.67,1.83-3.63,3.27-5.9,4.32
51 + C667.25,112.52,664.79,113.05,662.15,113.05z M662.2,107.56c1.96,0,3.7-0.4,5.22-1.19c1.52-0.79,2.8-1.88,3.83-3.23
52 + c1.04-1.36,1.82-2.88,2.37-4.57c0.54-1.69,0.81-3.38,0.81-5.08c0-1.81-0.28-3.57-0.84-5.27c-0.56-1.7-1.39-3.21-2.47-4.51
53 + c-1.09-1.3-2.38-2.35-3.89-3.13c-1.5-0.78-3.18-1.17-5.03-1.17c-1.95,0-3.69,0.4-5.22,1.19c-1.52,0.8-2.81,1.88-3.86,3.23
54 + c-1.05,1.36-1.86,2.87-2.42,4.54c-0.56,1.66-0.84,3.37-0.84,5.11c0,1.81,0.29,3.57,0.87,5.27c0.58,1.7,1.4,3.2,2.47,4.51
55 + c1.07,1.3,2.36,2.35,3.89,3.13C658.62,107.17,660.32,107.56,662.2,107.56z M662.97,99.35h5.87l11.74,13.42h-5.87L662.97,99.35z"/>
56 + <path class="st0" d="M702.96,113.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29c-1.29-1.81-2.23-3.88-2.82-6.2
57 + c-0.6-2.32-0.9-4.73-0.9-7.23V74.19h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
58 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
59 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06V74.19h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
60 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.32,1.76-3.04,3.14-5.14,4.16C708.52,112.54,705.97,113.05,702.96,113.05z"/>
61 + <path class="st0" d="M754.21,107.4v5.38h-26.46V74.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23v11.79H754.21z M743.4,70.17
62 + l-3.91-1.2l3.1-5.97h5.71L743.4,70.17z"/>
63 + <path class="st0" d="M790.84,102.83c0,2.07-0.53,3.84-1.6,5.33c-1.07,1.48-2.51,2.63-4.32,3.42c-1.81,0.79-3.81,1.19-5.98,1.19
64 + h-18.42V74.19h19.89c1.81,0,3.38,0.48,4.7,1.44c1.32,0.96,2.34,2.19,3.04,3.7c0.71,1.5,1.06,3.07,1.06,4.7
65 + c0,1.88-0.48,3.65-1.44,5.3c-0.96,1.65-2.33,2.87-4.1,3.67c2.21,0.65,3.96,1.84,5.24,3.56
66 + C790.2,98.28,790.84,100.37,790.84,102.83z M766.6,79.46v11.36h11.2c1.01,0,1.92-0.24,2.72-0.73c0.79-0.49,1.43-1.16,1.9-2.01
67 + c0.47-0.85,0.71-1.82,0.71-2.91c0-1.12-0.22-2.11-0.65-2.96c-0.43-0.85-1.02-1.52-1.77-2.01c-0.75-0.49-1.6-0.73-2.58-0.73H766.6z
68 + M784.6,101.69c0-1.08-0.23-2.07-0.68-2.96c-0.45-0.89-1.07-1.61-1.85-2.17c-0.78-0.56-1.69-0.84-2.74-0.84H766.6v11.79h12.34
69 + c1.08,0,2.06-0.26,2.91-0.79c0.85-0.53,1.52-1.23,2.01-2.12C784.35,103.71,784.6,102.74,784.6,101.69z"/>
70 + <path class="st0" d="M823.67,107.4v5.38H797.2V74.19h25.98v5.38h-19.89v10.98h17.23v5.05h-17.23v11.79H823.67z"/>
71 + <path class="st0" d="M826.82,93.16c0-2.35,0.42-4.66,1.25-6.93s2.06-4.33,3.67-6.19c1.61-1.87,3.58-3.35,5.9-4.46
72 + c2.32-1.1,4.95-1.66,7.88-1.66c3.48,0,6.48,0.79,9.02,2.36c2.54,1.58,4.42,3.6,5.65,6.06l-4.78,3.21
73 + c-0.72-1.56-1.64-2.79-2.75-3.7c-1.11-0.9-2.3-1.54-3.59-1.9c-1.29-0.36-2.54-0.54-3.78-0.54c-2.03,0-3.81,0.41-5.32,1.22
74 + c-1.52,0.82-2.8,1.89-3.83,3.23c-1.03,1.34-1.81,2.84-2.34,4.51c-0.53,1.66-0.79,3.33-0.79,5c0,1.85,0.31,3.63,0.92,5.35
75 + c0.62,1.72,1.48,3.24,2.58,4.57c1.11,1.32,2.42,2.36,3.94,3.12c1.52,0.76,3.19,1.14,5,1.14c1.23,0,2.52-0.22,3.86-0.65
76 + c1.34-0.43,2.58-1.12,3.72-2.04c1.14-0.92,2.04-2.15,2.69-3.67l5.06,2.88c-0.8,1.92-2.04,3.55-3.72,4.89
77 + c-1.69,1.34-3.57,2.36-5.65,3.05c-2.08,0.69-4.14,1.03-6.17,1.03c-2.72,0-5.2-0.57-7.45-1.71c-2.25-1.14-4.18-2.66-5.81-4.57
78 + c-1.63-1.9-2.9-4.04-3.81-6.41C827.28,97.99,826.82,95.58,826.82,93.16z"/>
79 + <path class="st0" d="M579.38,160.4v5.38h-26.46v-38.59h25.98v5.38H559v10.98h17.23v5.05H559v11.79H579.38z"/>
80 + <path class="st0" d="M591.77,138.49v27.28h-6.08v-38.59h5l21.85,27.88v-27.83h6.14v38.53h-5.32L591.77,138.49z"/>
81 + <path class="st0" d="M656.83,166.05c-2.79,0-5.31-0.54-7.55-1.63c-2.25-1.08-4.18-2.56-5.82-4.43c-1.63-1.87-2.89-3.97-3.78-6.3
82 + c-0.89-2.34-1.33-4.74-1.33-7.2c0-2.57,0.47-5.03,1.41-7.37c0.94-2.34,2.25-4.42,3.91-6.25c1.66-1.83,3.63-3.28,5.9-4.35
83 + c2.26-1.07,4.72-1.6,7.36-1.6c2.75,0,5.26,0.56,7.53,1.69c2.26,1.12,4.2,2.63,5.82,4.51c1.61,1.88,2.86,3.99,3.75,6.3
84 + c0.89,2.32,1.33,4.69,1.33,7.12c0,2.57-0.46,5.03-1.39,7.36c-0.93,2.34-2.22,4.42-3.89,6.25c-1.67,1.83-3.63,3.27-5.9,4.32
85 + C661.92,165.52,659.47,166.05,656.83,166.05z M644.54,146.48c0,1.81,0.29,3.57,0.87,5.27c0.58,1.7,1.4,3.22,2.47,4.54
86 + c1.07,1.32,2.36,2.36,3.89,3.12c1.52,0.76,3.22,1.14,5.11,1.14c1.96,0,3.7-0.4,5.22-1.19c1.52-0.79,2.8-1.88,3.83-3.23
87 + c1.04-1.36,1.82-2.88,2.37-4.57c0.54-1.69,0.81-3.38,0.81-5.08c0-1.81-0.28-3.56-0.84-5.25c-0.56-1.69-1.39-3.19-2.47-4.51
88 + c-1.09-1.32-2.38-2.37-3.89-3.13c-1.5-0.76-3.18-1.14-5.03-1.14c-1.95,0-3.69,0.4-5.22,1.2c-1.52,0.79-2.81,1.87-3.86,3.2
89 + c-1.05,1.34-1.86,2.84-2.42,4.51C644.82,143.04,644.54,144.74,644.54,146.48z"/>
90 + <path class="st0" d="M697.75,166.05c-3.08,0-5.68-0.53-7.8-1.58s-3.82-2.48-5.11-4.29c-1.29-1.81-2.23-3.88-2.82-6.2
91 + c-0.6-2.32-0.9-4.73-0.9-7.23v-19.56h6.08v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
92 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
93 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06v-19.56h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
94 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.32,1.76-3.04,3.14-5.14,4.16C703.31,165.54,700.75,166.05,697.75,166.05z"/>
95 + <path class="st0" d="M750.85,132.57h-12.83v33.21h-6.14v-33.21H719v-5.38h31.85V132.57z"/>
96 + <path class="st0" d="M763.51,127.19h5.76l15,38.59h-6.41l-3.77-10.49h-15.43l-3.79,10.49h-6.47L763.51,127.19z M772.47,150.78
97 + l-6.08-16.9l-6.11,16.9H772.47z"/>
98 + <path class="st0" d="M803.62,166.05c-2.79,0-5.31-0.54-7.55-1.63c-2.25-1.08-4.18-2.56-5.82-4.43c-1.63-1.87-2.89-3.97-3.78-6.3
99 + c-0.89-2.34-1.33-4.74-1.33-7.2c0-2.57,0.47-5.03,1.41-7.37c0.94-2.34,2.25-4.42,3.91-6.25c1.66-1.83,3.63-3.28,5.9-4.35
100 + c2.26-1.07,4.72-1.6,7.36-1.6c2.75,0,5.26,0.56,7.53,1.69c2.26,1.12,4.2,2.63,5.82,4.51c1.61,1.88,2.86,3.99,3.75,6.3
101 + c0.89,2.32,1.33,4.69,1.33,7.12c0,2.57-0.46,5.03-1.39,7.36c-0.93,2.34-2.22,4.42-3.89,6.25c-1.67,1.83-3.63,3.27-5.9,4.32
102 + C808.71,165.52,806.26,166.05,803.62,166.05z M791.33,146.48c0,1.81,0.29,3.57,0.87,5.27c0.58,1.7,1.4,3.22,2.47,4.54
103 + c1.07,1.32,2.36,2.36,3.89,3.12c1.52,0.76,3.22,1.14,5.11,1.14c1.96,0,3.7-0.4,5.22-1.19c1.52-0.79,2.8-1.88,3.83-3.23
104 + c1.04-1.36,1.82-2.88,2.37-4.57c0.54-1.69,0.81-3.38,0.81-5.08c0-1.81-0.28-3.56-0.84-5.25c-0.56-1.69-1.39-3.19-2.47-4.51
105 + c-1.09-1.32-2.38-2.37-3.89-3.13c-1.5-0.76-3.18-1.14-5.03-1.14c-1.95,0-3.69,0.4-5.22,1.2c-1.52,0.79-2.81,1.87-3.86,3.2
106 + c-1.05,1.34-1.86,2.84-2.42,4.51C791.61,143.04,791.33,144.74,791.33,146.48z"/>
107 + <path class="st0" d="M844.54,166.05c-3.08,0-5.68-0.53-7.8-1.58c-2.12-1.05-3.82-2.48-5.11-4.29c-1.29-1.81-2.23-3.88-2.82-6.2
108 + c-0.6-2.32-0.9-4.73-0.9-7.23v-19.56H834v19.56c0,1.78,0.18,3.49,0.54,5.14c0.36,1.65,0.94,3.12,1.74,4.43
109 + c0.8,1.3,1.88,2.34,3.24,3.1c1.36,0.76,3.03,1.14,5.02,1.14c2.03,0,3.72-0.39,5.08-1.17c1.36-0.78,2.43-1.83,3.2-3.15
110 + c0.78-1.32,1.35-2.8,1.71-4.43c0.36-1.63,0.54-3.31,0.54-5.06v-19.56h6.14v19.56c0,2.65-0.32,5.14-0.95,7.47
111 + c-0.63,2.34-1.61,4.38-2.93,6.14c-1.33,1.76-3.04,3.14-5.14,4.16C850.1,165.54,847.55,166.05,844.54,166.05z"/>
112 + <path class="st0" d="M878.89,127.19h5.76l15,38.59h-6.41l-3.77-10.49h-15.43l-3.79,10.49h-6.47L878.89,127.19z M887.85,150.78
113 + l-6.08-16.9l-6.11,16.9H887.85z"/>
114 + <path class="st0" d="M904.43,165.78v-38.59h6.08v38.59H904.43z"/>
115 + <path class="st0" d="M942.04,136.27c-0.36-0.4-0.89-0.82-1.58-1.28c-0.69-0.45-1.51-0.88-2.45-1.28c-0.94-0.4-1.96-0.72-3.05-0.98
116 + c-1.08-0.25-2.21-0.38-3.37-0.38c-2.54,0-4.42,0.47-5.65,1.41c-1.23,0.94-1.85,2.24-1.85,3.9c0,1.23,0.34,2.19,1.03,2.9
117 + c0.69,0.71,1.74,1.29,3.15,1.76c1.41,0.47,3.19,0.98,5.33,1.52c2.61,0.61,4.87,1.35,6.79,2.19c1.92,0.85,3.39,1.98,4.4,3.38
118 + s1.52,3.27,1.52,5.58c0,1.95-0.36,3.63-1.09,5.04c-0.72,1.41-1.74,2.55-3.04,3.44c-1.3,0.88-2.81,1.53-4.51,1.95
119 + c-1.7,0.41-3.53,0.62-5.49,0.62c-1.92,0-3.83-0.2-5.73-0.6c-1.9-0.4-3.71-0.99-5.43-1.77c-1.72-0.78-3.31-1.73-4.76-2.85
120 + l2.77-5.22c0.47,0.47,1.15,0.99,2.04,1.55s1.92,1.1,3.1,1.6c1.18,0.51,2.46,0.93,3.86,1.28c1.4,0.35,2.82,0.52,4.26,0.52
121 + c2.43,0,4.29-0.42,5.57-1.27c1.29-0.84,1.93-2.06,1.93-3.65c0-1.3-0.42-2.34-1.25-3.12s-2.05-1.44-3.64-1.98
122 + c-1.6-0.54-3.5-1.08-5.71-1.63c-2.54-0.69-4.66-1.43-6.36-2.25c-1.7-0.81-2.98-1.86-3.83-3.14c-0.85-1.28-1.28-2.92-1.28-4.91
123 + c0-2.53,0.61-4.67,1.85-6.42c1.23-1.75,2.91-3.07,5.03-3.96c2.12-0.88,4.5-1.33,7.15-1.33c1.74,0,3.38,0.19,4.94,0.57
124 + c1.56,0.38,3.02,0.9,4.38,1.55c1.36,0.65,2.6,1.4,3.72,2.23L942.04,136.27z"/>
125 + </g>
126 +</g>
127 +<g>
128 + <path class="st0" d="M197.45,82.58c6.69-22.42,30.37-35.21,52.79-28.52c22.42,6.69,35.21,30.37,28.52,52.79
129 + c-5.55,18.59-22.79,30.55-41.29,30.28l38.37,28.56c18.32-9.74,32.9-26.52,39.3-47.98c12.7-42.54-11.5-87.32-54.04-100.02
130 + c-42.54-12.7-87.32,11.5-100.02,54.04c-10.01,33.55,2.93,68.48,29.71,87.98l44.94,33.45l0,0c17.7,13.65,31.15,11.87,36.39,10.51
131 + l29.44-8.83l-88.73-66.04C198.91,118.45,192.21,100.13,197.45,82.58z"/>
132 + <path class="st0" d="M408.09,14.15c-44.4,0-80.39,35.99-80.39,80.39c0,44.4,35.99,80.39,80.39,80.39
133 + c44.4,0,80.39-35.99,80.39-80.39C488.48,50.14,452.49,14.15,408.09,14.15z M408.09,136.96c-23.39,0-42.43-19.03-42.43-42.43
134 + c0-23.39,19.03-42.43,42.43-42.43c23.39,0,42.43,19.03,42.43,42.43C450.52,117.93,431.49,136.96,408.09,136.96z"/>
135 + <g>
136 + <g>
137 + <path class="st0" d="M109.85,24.34v80.71c0,4.45-0.45,8.42-1.32,11.79l-0.26,1c-0.11,0.37-0.27,0.84-0.48,1.41
138 + c-0.48,1.34-0.93,2.23-1.3,2.88c-2.89,5.53-8.81,12.35-20.71,14.15l-1.6,0.2c-0.92,0.11-1.76,0.17-2.55,0.18
139 + c-0.08,0-0.16-0.01-0.24-0.01l-0.26,0.01c-0.79-0.01-1.63-0.06-2.54-0.18l-1.5-0.18c-11.97-1.77-17.92-8.62-20.89-14.3
140 + c-0.29-0.51-0.74-1.41-1.24-2.8c-0.19-0.52-0.34-0.96-0.45-1.31l-0.27-1.05c-0.88-3.37-1.32-7.34-1.32-11.79V24.34H14.52v75.83
141 + c0.01,3.49,0.25,21.46,4.3,32.21c10.09,25.93,34.59,42.68,62.41,42.68h0.29c27.82,0,52.32-16.75,62.43-42.73
142 + c4.02-10.69,4.27-28.67,4.28-32.18V24.34H109.85z"/>
143 + </g>
144 + </g>
145 +</g>
146 +</svg>
added public/uqo/uqo-complet-bleu.png +0 −0

Binary file not shown.

deleted public/vercel.svg +0 −1
@@ -1 +0,0 @@
1 −<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
\ No newline at end of file
deleted public/window.svg +0 −1
@@ -1 +0,0 @@
1 −<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
modified src/app/HomeClient.tsx +83 −24
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — page d'accueil (landing du laboratoire pédagogique).
2 2 "use client";
3 3 import { useState } from "react";
4 4 import Link from "next/link";
@@ -127,43 +127,51 @@ export default function HomeClient({
127 127 {/* ================= HÉRO — terminal encre pleine largeur ================= */}
128 128 <section className="bleed vp-dark">
129 129 <div className="mx-auto max-w-6xl px-4 pb-10 pt-12 sm:px-6 sm:pb-14 sm:pt-16">
130 − <span className="kicker !text-[var(--accent-bright)] [&::before]:!bg-[var(--accent-bright)]">
130 + <div className="flex items-end gap-2.5">
131 + {/* logo officiel UQO — version blanche sur encre */}
132 + {/* eslint-disable-next-line @next/next/no-img-element */}
133 + <img src="/uqo/uqo-blanc.svg" alt="UQO" className="h-[34px] w-auto" />
134 + <span className="vp-display text-[26px] font-bold leading-none tracking-[-0.04em] text-[var(--accent-bright)]">
135 + Éval
136 + </span>
137 + </div>
138 + <span className="kicker mt-5 !text-[var(--accent-bright)] [&::before]:!bg-[var(--accent-bright)]">
131 139 {fr
132 − ? "Terminal public de valeur immobilière — Québec"
133 − : "Public real-estate value terminal — Québec"}
140 + ? "Laboratoire pédagogique d'évaluation immobilière — Québec"
141 + : "Real-estate valuation teaching laboratory — Québec"}
134 142 </span>
135 − <h1 className="vp-display mt-4 text-[clamp(38px,8vw,92px)] font-bold uppercase leading-[0.95] tracking-[-0.035em]">
143 + <h1 className="vp-display mt-4 text-[clamp(36px,7vw,84px)] font-bold uppercase leading-[0.95] tracking-[-0.035em]">
136 144 {fr ? (
137 145 <>
138 − La <span className="outline-txt">vraie</span> valeur.
146 + Comprendre la <span className="outline-txt">valeur</span>.
139 147 <br />
140 − <span className="text-[var(--accent-bright)]">Sans boîte noire.</span>
148 + <span className="text-[var(--accent-bright)]">Pas juste l&apos;obtenir.</span>
141 149 </>
142 150 ) : (
143 151 <>
144 − The <span className="outline-txt">true</span> value.
152 + Understand the <span className="outline-txt">value</span>.
145 153 <br />
146 − <span className="text-[var(--accent-bright)]">No black box.</span>
154 + <span className="text-[var(--accent-bright)]">Not just get it.</span>
147 155 </>
148 156 )}
149 157 </h1>
150 − <p className="mt-5 max-w-xl text-[15.5px] leading-relaxed text-[rgba(245,243,238,0.72)]">
158 + <p className="mt-5 max-w-xl text-[15.5px] leading-relaxed text-[rgba(233,237,238,0.72)]">
151 159 {t("sub")}
152 160 </p>
153 161
154 − {/* -------- la recherche : l'action centrale de la page -------- */}
162 + {/* -------- le CTA central : analyser une propriété -------- */}
155 163 <div className="mt-9 max-w-3xl">
156 − <p className="vp-mono mb-1 text-[10.5px] font-medium uppercase tracking-[0.16em] text-[rgba(245,243,238,0.55)]">
157 − {fr ? "Entrez une adresse au Québec" : "Enter a Québec address"}
164 + <p className="vp-mono mb-1 text-[10.5px] font-medium uppercase tracking-[0.16em] text-[rgba(233,237,238,0.6)]">
165 + {fr ? "Analyser une propriété — entrez une adresse au Québec" : "Analyze a property — enter a Québec address"}
158 166 </p>
159 167 <SearchBox variant="hero" />
160 − <p className="vp-mono mt-2.5 text-[10px] uppercase tracking-[0.06em] text-[rgba(245,243,238,0.4)]">
168 + <p className="vp-mono mt-2.5 text-[10px] uppercase tracking-[0.06em] text-[rgba(233,237,238,0.4)]">
161 169 {t("dataNote")}
162 170 </p>
163 171 </div>
164 172
165 173 {/* -------- preuves de profondeur de données, intégrées -------- */}
166 − <div className="mt-10 grid grid-cols-2 gap-x-6 gap-y-5 border-t border-[rgba(245,243,238,0.16)] pt-6 sm:grid-cols-4">
174 + <div className="mt-10 grid grid-cols-2 gap-x-6 gap-y-5 border-t border-[rgba(233,237,238,0.16)] pt-6 sm:grid-cols-4">
167 175 {proofs.map(([num, label], i) => (
168 176 <div key={label}>
169 177 <p className="vp-display flex items-baseline gap-2 text-[clamp(20px,2.6vw,28px)] font-bold leading-none tracking-[-0.02em] text-[var(--paper-on-ink)]">
@@ -175,7 +183,7 @@ export default function HomeClient({
175 183 />
176 184 )}
177 185 </p>
178 − <p className="vp-mono mt-1.5 text-[9.5px] uppercase tracking-[0.12em] text-[rgba(245,243,238,0.5)]">
186 + <p className="vp-mono mt-1.5 text-[9.5px] uppercase tracking-[0.12em] text-[rgba(233,237,238,0.5)]">
179 187 {label}
180 188 </p>
181 189 </div>
@@ -189,14 +197,14 @@ export default function HomeClient({
189 197 <div className="grid gap-x-8 gap-y-4 sm:grid-cols-3">
190 198 {(fr
191 199 ? [
192 − ["01", "Cherchez l'adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."],
193 − ["02", "Deux sources croisées", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."],
194 − ["03", "Fourchette + confiance", "P10-P90, indice A-D — le calcul montré en entier."],
200 + ["01", "Analysez une adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."],
201 + ["02", "Voyez le calcul en entier", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."],
202 + ["03", "Apprenez les concepts", "P10-P90, indice A-D, mode étudiant — chaque notion expliquée."],
195 203 ]
196 204 : [
197 − ["01", "Search the address", "3.7M assessment units, typo-tolerant search."],
198 − ["02", "Two sources, crossed", "Hedonic model (690k sales) + real comparables adjusted in dollars."],
199 − ["03", "Range + confidence", "P10-P90, A-D index — the full math, shown."],
205 + ["01", "Analyze an address", "3.7M assessment units, typo-tolerant search."],
206 + ["02", "See the full computation", "Hedonic model (690k sales) + real comparables adjusted in dollars."],
207 + ["03", "Learn the concepts", "P10-P90, A-D index, student mode — every notion explained."],
200 208 ]
201 209 ).map(([num, title, body]) => (
202 210 <div key={num} className="flex items-start gap-3">
@@ -216,7 +224,7 @@ export default function HomeClient({
216 224 <section className="mt-12">
217 225 <SectionHead
218 226 num="01"
219 − kicker={fr ? "Indice Vrai-Prix" : "Vrai-Prix index"}
227 + kicker={fr ? "Indice UQO Éval" : "UQO Éval index"}
220 228 title={fr ? "Le pouls du marché québécois" : "The pulse of the Québec market"}
221 229 lede={
222 230 fr
@@ -406,10 +414,61 @@ export default function HomeClient({
406 414 </div>
407 415 </section>
408 416
409 − {/* ================= PLAN B — estimation sans adresse ================= */}
417 + {/* ================= FONCTIONNALITÉS — le laboratoire ================= */}
410 418 <section className="mt-14">
411 419 <SectionHead
412 420 num="04"
421 + kicker={fr ? "Le laboratoire" : "The laboratory"}
422 + title={fr ? "Explorer les mécanismes de l'évaluation" : "Explore the mechanics of valuation"}
423 + lede={
424 + fr
425 + ? "UQO Éval sert à explorer les mécanismes de l'évaluation immobilière à travers des données réelles et des modèles quantitatifs — chaque résultat est décomposé, expliqué et vérifiable."
426 + : "UQO Éval explores the mechanics of real-estate valuation through real data and quantitative models — every result is decomposed, explained and verifiable."
427 + }
428 + />
429 + <div className="mt-6 grid gap-x-8 gap-y-6 sm:grid-cols-2 lg:grid-cols-3">
430 + {(fr
431 + ? [
432 + ["Estimation immobilière", "Valeur marchande, intervalle P10-P90 et indice de confiance A-D pour 3,7 M de propriétés.", "/"],
433 + ["Comparables expliqués", "Carte et tableau des ventes retenues : distance, date, prix/m², ajustements en dollars, poids.", "/"],
434 + ["Analyse géographique", "Carte 3D des valeurs estimées, ventes du secteur, position dans la municipalité.", "/carte"],
435 + ["Modèles quantitatifs", "Modèle hédonique LightGBM (18 variables), régression quantile, indice de marché mensuel.", "/methodologie"],
436 + ["Décomposition de la valeur", "Terrain vs bâtiment, poids modèle vs comparables, historique 2021-2026 millésime par millésime.", "/"],
437 + ["Outils pédagogiques", "Mode étudiant, méthodologie de niveau rapport technique, assistante IA qui explique chaque calcul.", "/assistant"],
438 + ]
439 + : [
440 + ["Property estimation", "Market value, P10-P90 interval and A-D confidence index for 3.7M properties.", "/"],
441 + ["Comparables, explained", "Map and table of selected sales: distance, date, $/m², dollar adjustments, weights.", "/"],
442 + ["Geographic analysis", "3D map of estimated values, sector sales, position within the municipality.", "/carte"],
443 + ["Quantitative models", "LightGBM hedonic model (18 variables), quantile regression, monthly market index.", "/methodologie"],
444 + ["Value decomposition", "Land vs building, model vs comparables weights, 2021-2026 history vintage by vintage.", "/"],
445 + ["Teaching tools", "Student mode, technical-report-depth methodology, an AI assistant that explains every computation.", "/assistant"],
446 + ]
447 + ).map(([title, body, href]) => (
448 + <Link
449 + key={title}
450 + href={href}
451 + className="group border-t border-[var(--line)] pt-4"
452 + >
453 + <h3 className="vp-display text-[15px] font-bold uppercase tracking-[-0.01em] group-hover:text-accent-deep">
454 + {title}
455 + </h3>
456 + <p className="mt-1.5 text-[13px] leading-snug text-ink-2">{body}</p>
457 + </Link>
458 + ))}
459 + </div>
460 + {/* disclaimer pédagogique — visible dès l'accueil */}
461 + <p className="vp-mono mt-8 border-l-2 border-accent pl-4 text-[11px] uppercase leading-relaxed tracking-[0.05em] text-ink-2">
462 + {fr
463 + ? "Plateforme à vocation pédagogique, universitaire et expérimentale — cours IMM1003 et IMM1033, UQO. Les estimations ne constituent pas une évaluation immobilière professionnelle certifiée (OEAQ)."
464 + : "Educational, academic and experimental platform — courses IMM1003 and IMM1033, UQO. Estimates are not a certified professional appraisal (OEAQ)."}
465 + </p>
466 + </section>
467 +
468 + {/* ================= PLAN B — estimation sans adresse ================= */}
469 + <section className="mt-14">
470 + <SectionHead
471 + num="05"
413 472 kicker={fr ? "Plan B" : "Plan B"}
414 473 title={t("manualTitle")}
415 474 />
deleted src/app/api/auth/ka/callback/route.ts +0 −48
@@ -1,48 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Retour SSO KA ID : vérifie le state signé localement puis le ka_token du hub
3 −// (JWT HS256 — signature KA_SSO_SECRET, alg, iss, aud, exp), pose la session
4 −// locale (cookie httpOnly vp_ka_session signé HMAC-SHA256, 30 jours) et
5 −// redirige vers la destination demandée (ou /compte).
6 −import { NextRequest, NextResponse } from "next/server";
7 −import {
8 − KA_BASE_URL,
9 − SESSION_COOKIE,
10 − SESSION_DAYS,
11 − sessionFromClaims,
12 − verifyKaToken,
13 − verifyState,
14 −} from "@/lib/ka-auth";
15 −
16 −export function GET(req: NextRequest) {
17 − const q = req.nextUrl.searchParams;
18 − const state = verifyState(q.get("state") ?? "");
19 − if (!state) {
20 − return NextResponse.json(
21 − { error: "state invalide ou expiré — recommencez la connexion" },
22 − { status: 400 },
23 − );
24 − }
25 − const claims = verifyKaToken(q.get("ka_token") ?? "");
26 − if (!claims) {
27 − return NextResponse.json(
28 − { error: "jeton KA invalide ou expiré — recommencez la connexion" },
29 − { status: 401 },
30 − );
31 − }
32 − const made = sessionFromClaims(claims);
33 − if (!made) {
34 − return NextResponse.json(
35 − { error: "jeton KA incomplet (ka_id manquant)" },
36 − { status: 401 },
37 − );
38 − }
39 − const res = NextResponse.redirect(new URL(state.next, KA_BASE_URL));
40 − res.cookies.set(SESSION_COOKIE, made.value, {
41 − httpOnly: true,
42 − secure: KA_BASE_URL.startsWith("https"),
43 − sameSite: "lax",
44 − path: "/",
45 − maxAge: SESSION_DAYS * 86400,
46 − });
47 − return res;
48 −}
deleted src/app/api/auth/ka/login/route.ts +0 −22
@@ -1,22 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Départ SSO « Se connecter avec KA ID » : redirige vers le hub d'identité du
3 −// Groupe KA (groupe-ka.com) avec un state opaque signé localement (le state
4 −// transporte aussi la destination `next` de retour — chemin local seulement).
5 −import { NextRequest, NextResponse } from "next/server";
6 −import { CLIENT_ID, KA_BASE_URL, KA_HUB_URL, newState, sanitizeNext } from "@/lib/ka-auth";
7 −
8 −export function GET(req: NextRequest) {
9 − const next = sanitizeNext(req.nextUrl.searchParams.get("next"));
10 − const state = newState(next);
11 − if (!state || !process.env.KA_SSO_SECRET) {
12 − return NextResponse.json(
13 − { error: "SSO KA ID non configuré (KA_SSO_SECRET / KA_AUTH_SECRET manquant)" },
14 − { status: 503 },
15 − );
16 − }
17 − const url = new URL(`${KA_HUB_URL}/sso/authorize`);
18 − url.searchParams.set("client_id", CLIENT_ID);
19 − url.searchParams.set("redirect_uri", `${KA_BASE_URL}/api/auth/ka/callback`);
20 − url.searchParams.set("state", state);
21 − return NextResponse.redirect(url);
22 −}
deleted src/app/api/auth/logout/route.ts +0 −17
@@ -1,17 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Déconnexion : efface le cookie de session local (la session est sans
3 −// stockage serveur — supprimer le cookie suffit). POST seulement.
4 −import { NextResponse } from "next/server";
5 −import { KA_BASE_URL, SESSION_COOKIE } from "@/lib/ka-auth";
6 −
7 −export function POST() {
8 − const res = NextResponse.json({ ok: true });
9 − res.cookies.set(SESSION_COOKIE, "", {
10 − httpOnly: true,
11 − secure: KA_BASE_URL.startsWith("https"),
12 − sameSite: "lax",
13 − path: "/",
14 − maxAge: 0,
15 − });
16 − return res;
17 −}
deleted src/app/api/auth/me/route.ts +0 −37
@@ -1,37 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Session courante : { user } si le cookie vp_ka_session est valide, sinon
3 −// { user: null } — consommé par le bouton compte de l'en-tête et la page /compte.
4 −// La réponse est enrichie du profil saisi au HUB Groupe KA (bio, ville, emploi,
5 −// entreprise, âge, site web, réseaux, statut, profil public) quand il est
6 −// joignable — profile_source: "groupe-ka" ; sinon retombée sur la session
7 −// locale — profile_source: "local".
8 −import { NextRequest, NextResponse } from "next/server";
9 −import { SESSION_COOKIE, readSession } from "@/lib/ka-auth";
10 −import { fetchHubProfile } from "@/lib/hub-profile";
11 −
12 −export async function GET(req: NextRequest) {
13 − const s = readSession(req.cookies.get(SESSION_COOKIE)?.value);
14 − if (!s) return NextResponse.json({ user: null });
15 − const hub = await fetchHubProfile(s.ka_id); // cache 60 s, null si injoignable
16 − return NextResponse.json({
17 − user: {
18 − ka_id: s.ka_id,
19 − email: hub?.email || s.email,
20 − name: hub?.name || s.name,
21 − picture: hub?.picture || s.picture,
22 − provider: s.provider ?? "ka-id",
23 − // — profil Groupe KA (source de vérité : groupe-ka.com/compte) —
24 − bio: hub?.bio ?? "",
25 − city: hub?.city ?? "",
26 − job_title: hub?.job_title ?? "",
27 − company: hub?.company ?? "",
28 − age: hub?.age ?? null,
29 − website: hub?.website ?? "",
30 − socials: hub?.socials ?? {},
31 − role_label: hub?.role_label ?? "",
32 − public: hub?.public ?? false,
33 − public_url: hub?.public_url ?? "",
34 − profile_source: hub ? "groupe-ka" : "local",
35 − },
36 − });
37 −}
modified src/app/api/estimate/route.ts +1 −26
@@ -1,32 +1,12 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — API d'estimation (dérivée du moteur UQO Éval, adaptation pédagogique)
2 2 import { NextRequest, NextResponse } from "next/server";
3 3 import { estimateByUnitId, estimateManual } from "@/lib/estimator";
4 −import { CLIENT_ID, SESSION_COOKIE, readSession } from "@/lib/ka-auth";
5 −import { track } from "@/lib/kaid";
6 −
7 −// personnalisation KA ID v2 : une estimation est un signal d'intérêt fort —
8 −// vocabulaire aligné sur Immo·Ka (city, property_type) pour nourrir les
9 −// affinités transversales du membre (localisation, type de propriété).
10 −function kaIdOf(req: NextRequest): string | null {
11 − const s = readSession(req.cookies.get(SESSION_COOKIE)?.value);
12 − return s?.ka_id ?? null;
13 −}
14 4
15 5 export function GET(req: NextRequest) {
16 6 const id = req.nextUrl.searchParams.get("id");
17 7 if (!id) return NextResponse.json({ error: "id requis" }, { status: 400 });
18 8 const r = estimateByUnitId(id);
19 9 if (!r) return NextResponse.json({ error: "unité introuvable" }, { status: 404 });
20 − const rr = r as unknown as Record<string, unknown>;
21 − track(CLIENT_ID, kaIdOf(req), [{
22 − type: "detail_view",
23 − entity_type: "estimation",
24 − entity_id: id,
25 − features: {
26 − city: (rr.municipality as string) ?? undefined,
27 − property_type: (rr.typeProp as string) ?? undefined,
28 − },
29 − }]);
30 10 return NextResponse.json(r);
31 11 }
32 12
@@ -56,10 +36,5 @@ export async function POST(req: NextRequest) {
56 36 { error: "municipalité introuvable ou marché insuffisant" },
57 37 { status: 404 }
58 38 );
59 − track(CLIENT_ID, kaIdOf(req), [{
60 − type: "search",
61 − query: body.municipality,
62 − filters: { city: body.municipality, property_type: body.typeProp },
63 − }]);
64 39 return NextResponse.json(r);
65 40 }
deleted src/app/api/favorites/route.ts +0 −21
@@ -1,21 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// GET /api/favorites — favoris « Mon univers Ka » du membre connecté, lus au
3 −// hub Groupe KA (magasin central, cache serveur 30 s — voir lib/hubfav).
4 −// 401 sans session KA ID valide ; 502 si le hub est injoignable.
5 −import { NextRequest, NextResponse } from "next/server";
6 −import { SESSION_COOKIE, readSession } from "@/lib/ka-auth";
7 −import { hubList } from "@/lib/hubfav";
8 −
9 −export const dynamic = "force-dynamic";
10 −
11 −export async function GET(req: NextRequest) {
12 − const s = readSession(req.cookies.get(SESSION_COOKIE)?.value);
13 − if (!s) {
14 − return NextResponse.json({ error: "auth_required" }, { status: 401 });
15 − }
16 − const favorites = await hubList(s.ka_id);
17 − if (favorites === null) {
18 − return NextResponse.json({ error: "hub_unreachable" }, { status: 502 });
19 − }
20 − return NextResponse.json({ favorites, count: favorites.length });
21 −}
deleted src/app/api/favorites/toggle/route.ts +0 −44
@@ -1,44 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// POST /api/favorites/toggle — bascule un ♥ « Mon univers Ka » au hub Groupe KA.
3 −// Corps : { item_id, title?, subtitle?, price_label?, image_url?, url?, meta?,
4 −// action? } — sans `action` explicite (« add » / « remove »), on bascule selon
5 −// la présence de l'item dans la liste du hub. SYNCHRONE (aucun stockage
6 −// local) : 401 sans session, 400 item invalide, 502 hub injoignable/refus.
7 −import { NextRequest, NextResponse } from "next/server";
8 −import { SESSION_COOKIE, readSession } from "@/lib/ka-auth";
9 −import { cleanItem, hubList, hubToggle } from "@/lib/hubfav";
10 −
11 −export const dynamic = "force-dynamic";
12 −
13 −export async function POST(req: NextRequest) {
14 − const s = readSession(req.cookies.get(SESSION_COOKIE)?.value);
15 − if (!s) {
16 − return NextResponse.json({ error: "auth_required" }, { status: 401 });
17 − }
18 − let body: unknown;
19 − try {
20 − body = await req.json();
21 − } catch {
22 − return NextResponse.json({ error: "invalid_json" }, { status: 400 });
23 − }
24 − const item = cleanItem(body);
25 − if (!item) {
26 − return NextResponse.json({ error: "item_id_required" }, { status: 400 });
27 − }
28 − const requested = (body as { action?: unknown }).action;
29 − let action: "add" | "remove";
30 − if (requested === "add" || requested === "remove") {
31 − action = requested;
32 − } else {
33 − const list = await hubList(s.ka_id);
34 − if (list === null) {
35 − return NextResponse.json({ error: "hub_unreachable" }, { status: 502 });
36 − }
37 − action = list.some((f) => f.item_id === item.item_id) ? "remove" : "add";
38 − }
39 − const ok = await hubToggle(s.ka_id, action, item);
40 − if (!ok) {
41 − return NextResponse.json({ error: "hub_error" }, { status: 502 });
42 − }
43 − return NextResponse.json({ ok: true, favorited: action === "add" });
44 −}
modified src/app/api/ka/route.ts +13 −13
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Ka — agent IA de Vrai-Prix. Boucle agentique en streaming :
3 + * Éva — assistante pédagogique d'UQO Éval. Boucle agentique en streaming :
4 4 * Claude Haiku 4.5 + panoplie d'outils serveur (base SQLite + moteur).
5 5 * La réponse est un flux SSE : deltas de texte, activité d'outils, fin.
6 6 * Le prompt système et les outils sont mis en cache (prompt caching).
@@ -16,25 +16,25 @@ const MAX_TURNS = 12;
16 16 const MAX_MESSAGES = 40;
17 17 const MAX_CHARS = 4000;
18 18
19 −const SYSTEM = `Tu es Ka, l'agent d'évaluation immobilière de Vrai-Prix (www.vrai-prix.com), créé par Simon-Pierre Boucher. Tu aides les gens à connaître la vraie valeur de leur propriété au Québec — unifamiliale, condo, plex, chalet, terrain — en t'appuyant sur 3 747 008 unités d'évaluation, 745 119 ventes réelles (2021-2026) et un modèle hédonique validé (erreur médiane 11 %).
19 +const SYSTEM = `Tu es Éva, l'assistante pédagogique d'UQO Éval (www.uqo-eval.app), le laboratoire pédagogique d'évaluation immobilière développé dans le cadre des cours IMM1003 — Éléments d'évaluation immobilière et IMM1033 — Méthodes du coût en évaluation immobilière, enseignés par Simon-Pierre Boucher (Université du Québec en Outaouais). Tu aides les étudiantes et étudiants à COMPRENDRE comment une propriété est évaluée au Québec — unifamiliale, condo, plex, chalet, terrain — en t'appuyant sur 3 747 008 unités d'évaluation, 745 119 ventes réelles (2021-2026) et un modèle hédonique validé (erreur médiane 11 %).
20 20
21 21 # Ta démarche
22 22 1. Quand on te donne une adresse (même partielle), appelle chercher_propriete immédiatement.
23 23 2. S'il y a UN candidat clair, évalue-le directement (evaluer_propriete). S'il y en a plusieurs plausibles, liste-les brièvement (adresse, municipalité, type) et demande à l'utilisateur de choisir — ne devine jamais.
24 24 2b. Si la recherche revient avec correspondance « approximative », ne dis JAMAIS juste « introuvable » : présente les 2-4 adresses les plus proches (« je n'ai pas trouvé exactement ça, mais voici ce qui s'en rapproche ») et demande si c'est l'une d'elles. Utilise l'id EXACT retourné par l'outil — ne le retape jamais de mémoire.
25 −3. Dès qu'une unité est confirmée : appelle dossier_complet — il te donne TOUT en un appel (évaluation, marché local, tendance, comparables, synthèse chiffrée). C'est ta frappe signature.
26 −4. Livre un mini-rapport d'expert structuré : la valeur en évidence (fourchette P10-P90, confiance A-D expliquée), puis le contexte (écart vs rôle, position vs médiane municipale, $/m² vs comparables), puis 3 insights chiffrés que l'utilisateur ne savait pas (croissance vs marché, comparables marquants, momentum). Chaque réponse doit impressionner un courtier.
27 −5. Les liens des rapports PDF sont DÉJÀ dans chaque évaluation/dossier (champ « liens ») : quand l'utilisateur veut le PDF, donne ces liens directement en markdown ([Rapport standard](url) / [Rapport professionnel](url)) — n'appelle liens_rapports que si tu ne les as plus sous la main. Termine en proposant une suite concrète (comparer, parc, secteur).
25 +3. Dès qu'une unité est confirmée : appelle dossier_complet — il te donne TOUT en un appel (évaluation, marché local, tendance, comparables, synthèse chiffrée).
26 +4. Livre une explication structurée et PÉDAGOGIQUE : la valeur en évidence (fourchette P10-P90, indice de confiance A-D expliqué), puis le POURQUOI — comment les comparables ont été choisis, ce que chaque ajustement (marché, superficie, âge) représente, la pondération 65 % modèle / 35 % comparables, l'écart vs rôle et ce qu'il signifie. Profite de chaque réponse pour ancrer un concept d'évaluation (valeur marchande vs prix demandé vs prix de transaction, méthode des comparables, modèle hédonique, dispersion…).
27 +5. Les liens des rapports PDF sont DÉJÀ dans chaque évaluation/dossier (champ « liens ») : quand l'utilisateur veut le PDF, donne ces liens directement en markdown ([Rapport standard](url) / [Rapport professionnel](url)) — n'appelle liens_rapports que si tu ne les as plus sous la main. Termine en proposant une suite concrète (comparer deux propriétés, analyser un secteur, explorer la méthodologie).
28 28
29 29 # Frappe fort
30 30 - Quand plusieurs recherches indépendantes sont nécessaires (ex. deux adresses, ou stats + indice), appelle PLUSIEURS outils dans le MÊME tour — ils s'exécutent en parallèle.
31 −- Ne te contente jamais du minimum : croise les données (propriété vs marché local vs tendance provinciale) et tranche avec un avis chiffré.
31 +- Ne te contente jamais du minimum : croise les données (propriété vs marché local vs tendance provinciale) et explique ce que les chiffres veulent dire.
32 32 - Anticipe la prochaine question : offre-la avant qu'on te la pose.
33 33
34 34 # Ton style
35 −- Langue de l'utilisateur (français québécois par défaut). Tutoiement chaleureux mais professionnel.
35 +- Langue de l'utilisateur (français québécois par défaut). Tutoiement chaleureux mais professionnel — le ton d'une auxiliaire d'enseignement.
36 36 - Concis et concret : des chiffres, pas du remplissage. Formate les montants à la québécoise (ex. 445 000 $) et arrondis intelligemment (au millier près).
37 −- Transparence totale : c'est l'ADN de Vrai-Prix. Explique d'où viennent les chiffres quand on te le demande.
37 +- Transparence totale : c'est l'ADN d'UQO Éval. Explique d'où viennent les chiffres — c'est le cœur de ta mission pédagogique.
38 38 - Markdown léger : gras pour la valeur clé, listes courtes. Tableaux markdown permis mais compacts (max 5 colonnes, libellés courts).
39 39
40 40 # Continuité et intégrité (à respecter strictement)
@@ -43,8 +43,8 @@ const SYSTEM = `Tu es Ka, l'agent d'évaluation immobilière de Vrai-Prix (www.v
43 43 - Pas de réf ni de liens d'outil sous la main ? Tu DOIS appeler chercher puis liens_rapports AVANT de donner un lien — jamais de lien de mémoire.
44 44
45 45 # Limites (à respecter strictement)
46 −- Estimations statistiques à titre indicatif : rappelle au besoin que ça ne remplace pas un évaluateur agréé (OEAQ) ni un courtier.
47 −- Ne réponds qu'aux sujets liés à l'immobilier québécois et à Vrai-Prix. Pour tout autre sujet, ramène poliment la conversation.
46 +- Plateforme pédagogique, universitaire et expérimentale : rappelle au besoin que les estimations ne remplacent pas une évaluation par un évaluateur agréé (OEAQ).
47 +- Ne réponds qu'aux sujets liés à l'immobilier québécois, à l'évaluation immobilière et à UQO Éval. Pour tout autre sujet, ramène poliment la conversation.
48 48 - N'invente JAMAIS un chiffre : tout montant cité doit venir d'un outil. Si un outil échoue, dis-le simplement.
49 49 - Tu ne peux pas voir l'intérieur des propriétés : les rénovations récentes ne sont pas captées, mentionne-le si pertinent.`;
50 50
@@ -153,8 +153,8 @@ export async function POST(req: Request): Promise<Response> {
153 153 refs: [...refs].slice(-4).map(([id, adresse]) => ({
154 154 id,
155 155 adresse,
156 − pdf: `https://www.vrai-prix.com/api/report?id=${encodeURIComponent(id)}`,
157 − pdf_pro: `https://www.vrai-prix.com/api/report/pro?id=${encodeURIComponent(id)}`,
156 + pdf: `https://www.uqo-eval.app/api/report?id=${encodeURIComponent(id)}`,
157 + pdf_pro: `https://www.uqo-eval.app/api/report/pro?id=${encodeURIComponent(id)}`,
158 158 })),
159 159 });
160 160 }
deleted src/app/api/lead/route.ts +0 −17
@@ -1,17 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −import { NextRequest, NextResponse } from "next/server";
3 −import { saveLead } from "@/lib/db";
4 −
5 −export async function POST(req: NextRequest) {
6 − const body = (await req.json()) as {
7 − email?: string;
8 − unitId?: string;
9 − estimate?: number;
10 − };
11 − const email = body.email?.trim() ?? "";
12 − if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) {
13 − return NextResponse.json({ error: "courriel invalide" }, { status: 400 });
14 − }
15 − saveLead(email, body.unitId ?? null, body.estimate ?? null);
16 − return NextResponse.json({ ok: true });
17 −}
modified src/app/api/nearby/route.ts +2 −2
@@ -1,10 +1,10 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Recherche par carte : unités d'évaluation autour d'un point (boîte englobante).
3 3 // Le rayon est plafonné côté serveur — la carte doit être assez zoomée.
4 4 import { NextRequest, NextResponse } from "next/server";
5 5 import { unitsNear } from "@/lib/db";
6 6
7 −// Plafonds relevés pour Vrai-Prix Maps (Ka Maps) : les valeurs par défaut
7 +// Plafonds relevés pour UQO Éval Maps (Ka Maps) : les valeurs par défaut
8 8 // restent inchangées — l'app iOS 1.1.0 en place reçoit exactement le même
9 9 // contrat (changement additif ; ses gouttières disparaissent même).
10 10 const MAX_HALF_LAT = 0.08; // ~8,9 km
modified src/app/api/portfolio/route.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import { NextRequest, NextResponse } from "next/server";
3 3 import { estimatePortfolio } from "@/lib/estimator";
4 4
modified src/app/api/report/portfolio/route.ts +2 −2
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import { NextRequest, NextResponse } from "next/server";
3 3 import { estimatePortfolio } from "@/lib/estimator";
4 4 import { buildPortfolioReport } from "@/lib/report-portfolio";
@@ -18,7 +18,7 @@ export async function GET(req: NextRequest) {
18 18 return new NextResponse(new Uint8Array(pdf), {
19 19 headers: {
20 20 "Content-Type": "application/pdf",
21 − "Content-Disposition": `attachment; filename="vrai-prix-parc-${pf.items.length}-proprietes.pdf"`,
21 + "Content-Disposition": `attachment; filename="uqo-eval-parc-${pf.items.length}-proprietes.pdf"`,
22 22 "Cache-Control": "private, max-age=300",
23 23 },
24 24 });
modified src/app/api/report/pro/route.ts +2 −2
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import { NextRequest, NextResponse } from "next/server";
3 3 import { estimateByUnitId, indexType } from "@/lib/estimator";
4 4 import { getMarketIndex } from "@/lib/db";
@@ -22,7 +22,7 @@ export async function GET(req: NextRequest) {
22 22 return new NextResponse(new Uint8Array(pdf), {
23 23 headers: {
24 24 "Content-Type": "application/pdf",
25 − "Content-Disposition": `attachment; filename="vrai-prix-rapport-detaille-${slug}.pdf"`,
25 + "Content-Disposition": `attachment; filename="uqo-eval-rapport-detaille-${slug}.pdf"`,
26 26 "Cache-Control": "private, max-age=300",
27 27 },
28 28 });
modified src/app/api/report/route.ts +2 −2
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import { NextRequest, NextResponse } from "next/server";
3 3 import { estimateByUnitId } from "@/lib/estimator";
4 4 import { buildReport } from "@/lib/report";
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest) {
25 25 return new NextResponse(new Uint8Array(pdf), {
26 26 headers: {
27 27 "Content-Type": "application/pdf",
28 − "Content-Disposition": `attachment; filename="vrai-prix-${slug}.pdf"`,
28 + "Content-Disposition": `attachment; filename="uqo-eval-${slug}.pdf"`,
29 29 "Cache-Control": "private, max-age=300",
30 30 },
31 31 });
modified src/app/api/report/stats/route.ts +2 −2
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Alias historique de /api/stats/report (gabarit commun Groupe KA v2).
3 + * Alias historique de /api/stats/report (gabarit stats v2).
4 4 * Sert le même rapport PDF, mêmes paramètres
5 5 * (`mode=complet|synthese|tendances|repartitions|donnees`).
6 6 */
modified src/app/api/search/route.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import { NextRequest, NextResponse } from "next/server";
3 3 import { searchUnits } from "@/lib/db";
4 4
modified src/app/api/stats/catalog/route.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * GET /api/stats/catalog — v3 : blocs composables pour le constructeur de
4 4 * rapports personnalisés (src/ka/stats/SPEC.md §3bis). Dérivé du dashboard
modified src/app/api/stats/dashboard/route.ts +4 −4
@@ -1,9 +1,9 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * GET /api/stats/dashboard — contrat commun Groupe KA (src/ka/stats/SPEC.md §2).
4 − * Consommé par le hub groupe-ka.com et par le rapport PDF /api/stats/report.
3 + * GET /api/stats/dashboard — contrat stats v2 (src/ka/stats/SPEC.md §2).
4 + * Consommé par le rapport PDF /api/stats/report.
5 5 *
6 − * Particularité Vrai-Prix : les données sont un INSTANTANÉ du rôle d'évaluation
6 + * Particularité UQO Éval : les données sont un INSTANTANÉ du rôle d'évaluation
7 7 * foncière 2026 (pas de séries quotidiennes). `period`, `from` et `to` sont
8 8 * acceptés (contrat commun) mais NON APPLICABLES : la réponse est toujours
9 9 * l'instantané, avec `period.label = "instantané du rôle 2026"` et
modified src/app/api/stats/report/custom/route.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * POST /api/stats/report/custom — v3 : rapport PDF personnalisé
4 4 * (src/ka/stats/SPEC.md §3bis). Corps : {"title", "period", "from", "to",
modified src/app/api/stats/report/route.ts +2 −2
@@ -1,7 +1,7 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * GET /api/stats/report?mode=complet|synthese|tendances|repartitions|donnees
4 − * — rapport statistique PDF au gabarit commun Groupe KA v2 (src/ka/stats/
4 + * — rapport statistique PDF au gabarit stats v2 (src/ka/stats/
5 5 * SPEC.md §3), moteur pdfkit vectoriel. Un mode inconnu retombe sur
6 6 * « complet ». `period`/`from`/`to` sont acceptés (contrat commun) mais non
7 7 * applicables : les données sont un instantané du rôle 2026.
modified src/app/api/stats/route.ts +4 −4
@@ -1,7 +1,7 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * GET /api/stats — agrégats provinciaux publics de Vrai-Prix.
4 − * Consommé en live par le site mère groupe-ka.com pour ses métriques.
3 + * GET /api/stats — agrégats provinciaux publics de UQO Éval.
4 + * Agrégats publics de la plateforme.
5 5 * Import statique de src/data/stats.json (mêmes données que la page /stats) :
6 6 * aucune base de données, réponse cachable 10 minutes.
7 7 */
@@ -23,7 +23,7 @@ export function GET() {
23 23 transactions: TRANSACTIONS,
24 24 model_version: "hédonique LightGBM + comparables ajustés — erreur médiane 11 %",
25 25 updated: stats.generated,
26 − source: "https://www.vrai-prix.com/stats",
26 + source: "https://www.uqo-eval.app/stats",
27 27 },
28 28 {
29 29 headers: {
renamed src/app/ka/KaHero.tsx → src/app/assistant/KaHero.tsx +22 −23
@@ -1,14 +1,13 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — page /assistant : présentation d'Éva, l'assistante pédagogique.
2 2 /**
3 − * Page /ka — présentation de l'agent. Le chat lui-même est le WIDGET COMMUN
4 − * de l'écosystème (public/ka-agent.js, synchronisé depuis ka-ui) : la bulle
5 − * en bas à droite, qui s'ouvre plein écran. Cette page ne fait que le
6 − * présenter et le déclencher — plus aucun fil de conversation embarqué.
3 + * Le chat lui-même est le widget commun (public/assistant.js) : la bulle en
4 + * bas à droite, qui s'ouvre plein écran. Cette page ne fait que le présenter
5 + * et le déclencher — aucun fil de conversation embarqué.
7 6 */
8 7 "use client";
9 8 import { useLang } from "@/components/LangContext";
10 9
11 −/** Ouvre le widget ka-agent (plein écran d'office) en cliquant sa bulle. */
10 +/** Ouvre le widget assistant (plein écran d'office) en cliquant sa bulle. */
12 11 function openKa() {
13 12 const btn = document.querySelector<HTMLButtonElement>(".kaa-btn");
14 13 btn?.click();
@@ -34,50 +33,50 @@ export default function KaHero() {
34 33
35 34 const examples = fr
36 35 ? [
37 − "Combien vaut le 1075, chemin Sainte-Foy à Québec ?",
38 − "Le marché des condos à Montréal, ça va où ?",
39 − "Trouve-moi les dernières ventes à Lévis.",
36 + "Combien vaut le 1075, chemin Sainte-Foy à Québec — et pourquoi ?",
37 + "Explique-moi comment les comparables de cette estimation ont été choisis.",
38 + "Quelle est la différence entre valeur marchande et évaluation municipale ?",
40 39 ]
41 40 : [
42 − "What is 1075 chemin Sainte-Foy in Québec City worth?",
43 − "Where is the Montréal condo market heading?",
44 − "Find me the latest sales in Lévis.",
41 + "What is 1075 chemin Sainte-Foy in Québec City worth — and why?",
42 + "Explain how the comparables of this estimate were selected.",
43 + "What is the difference between market value and assessed value?",
45 44 ];
46 45
47 46 return (
48 47 <main className="relative z-[1] mx-auto w-full max-w-6xl px-4 pb-16 pt-12 sm:px-6">
49 48 <span className="kicker">
50 − {fr ? "Agent IA · propulsé par Claude" : "AI agent · powered by Claude"}
49 + {fr ? "Assistante pédagogique · propulsée par Claude" : "Teaching assistant · powered by Claude"}
51 50 </span>
52 51 <h1 className="vp-display mt-3 max-w-3xl text-[clamp(32px,5.5vw,58px)] font-bold uppercase leading-[0.98] tracking-[-0.03em]">
53 52 {fr ? (
54 53 <>
55 − Jase avec <span className="hl">Ka</span>.
54 + Demandez à <span className="hl">Éva</span>.
56 55 <br />
57 − Il connaît chaque adresse du Québec.
56 + Elle explique chaque évaluation.
58 57 </>
59 58 ) : (
60 59 <>
61 − Chat with <span className="hl">Ka</span>.
60 + Ask <span className="hl">Éva</span>.
62 61 <br />
63 − It knows every Québec address.
62 + She explains every appraisal.
64 63 </>
65 64 )}
66 65 </h1>
67 66 <p className="mt-4 max-w-2xl text-[15.5px] leading-relaxed text-ink-2">
68 67 {fr
69 − ? "Donne-lui une adresse : il la retrouve dans le registre, l'évalue avec le même moteur transparent que Vrai-Prix, puis t'explique le prix — comparables, marché local, tendance. Même agent sur tous les sites du Groupe KA."
70 − : "Give it an address: it finds it in the registry, values it with Vrai-Prix's transparent engine, then explains the price — comparables, local market, trend. Same agent across every Groupe KA site."}
68 + ? "Donnez-lui une adresse : elle la retrouve dans le registre, l'évalue avec le moteur transparent d'UQO Éval, puis vous explique le calcul — comparables, ajustements, marché local, indice de confiance. Un tuteur d'évaluation immobilière, disponible en tout temps."
69 + : "Give her an address: she finds it in the registry, values it with UQO Éval's transparent engine, then explains the computation — comparables, adjustments, local market, confidence index. A real-estate valuation tutor, available anytime."}
71 70 </p>
72 71
73 72 <div className="mt-8 flex flex-wrap items-center gap-4">
74 73 <button type="button" onClick={openKa} className="btn btn-accent !px-8 !text-[15px]">
75 − {fr ? "Parler à Ka — plein écran" : "Talk to Ka — full screen"} →
74 + {fr ? "Parler à Éva — plein écran" : "Talk to Éva — full screen"} →
76 75 </button>
77 76 <p className="vp-mono max-w-[260px] text-[10.5px] uppercase tracking-[0.06em] text-ink-3">
78 77 {fr
79 − ? "Ou touchez la bulle Ka, en bas à droite — sur toutes les pages."
80 − : "Or tap the Ka bubble, bottom right — on every page."}
78 + ? "Ou touchez la bulle Éva, en bas à droite — sur toutes les pages."
79 + : "Or tap the Éva bubble, bottom right — on every page."}
81 80 </p>
82 81 </div>
83 82
@@ -107,7 +106,7 @@ export default function KaHero() {
107 106 <span className="flex items-baseline justify-between gap-4">
108 107 <span className="text-[15px] font-semibold">{q}</span>
109 108 <span className="vp-mono shrink-0 text-[10.5px] font-bold uppercase tracking-[0.08em] text-accent">
110 − {fr ? "Demander à Ka" : "Ask Ka"} →
109 + {fr ? "Demander à Éva" : "Ask Éva"} →
111 110 </span>
112 111 </span>
113 112 </button>
added src/app/assistant/page.tsx +14 −0
@@ -0,0 +1,14 @@
1 +// UQO Éval — page /assistant (Éva, l'assistante pédagogique).
2 +import type { Metadata } from "next";
3 +import KaHero from "./KaHero";
4 +
5 +export const metadata: Metadata = {
6 + title: "Éva — l'assistante pédagogique qui explique chaque évaluation",
7 + description:
8 + "Éva évalue n'importe quelle propriété du Québec en conversant et explique le calcul : recherche dans le registre, évaluation transparente, comparables, ajustements, marché — en direct.",
9 + alternates: { canonical: "/assistant" },
10 +};
11 +
12 +export default function AssistantPage() {
13 + return <KaHero />;
14 +}
modified src/app/carte/CartePage.tsx +4 −5
@@ -1,10 +1,9 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Projet : Groupe Ka / Ka Maps (intégration Vrai-Prix)
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
3 2 "use client";
4 3 import dynamic from "next/dynamic";
5 4 import { useLang } from "@/components/LangContext";
6 5
7 −// Vrai-Prix Maps (framework Ka Maps) — Mapbox GL est navigateur-seul :
6 +// UQO Éval Maps (framework Ka Maps) — Mapbox GL est navigateur-seul :
8 7 // chargé dynamiquement sans SSR (voir docs Next : lazy-loading).
9 8 const KaCarte = dynamic(() => import("@/components/KaCarte"), {
10 9 ssr: false,
@@ -37,8 +36,8 @@ export default function CartePage() {
37 36 </div>
38 37 <p className="vp-mono mt-5 text-[10.5px] uppercase tracking-[0.06em] text-ink-3">
39 38 {fr
40 − ? "Vrai-Prix Maps — framework Ka Maps par Groupe Ka · Fond © Mapbox © OpenStreetMap · Estimations statistiques à titre indicatif (OEAQ)."
41 − : "Vrai-Prix Maps — Ka Maps framework by Groupe Ka · Basemap © Mapbox © OpenStreetMap · Statistical estimates for information only (OEAQ)."}
39 + ? "UQO Éval Maps · Fond © Mapbox © OpenStreetMap · Estimations statistiques à titre indicatif — plateforme pédagogique (OEAQ)."
40 + : "UQO Éval Maps · Basemap © Mapbox © OpenStreetMap · Statistical estimates for information only — teaching platform (OEAQ)."}
42 41 </p>
43 42 </div>
44 43 );
modified src/app/carte/page.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import CartePage from "./CartePage";
3 3
4 4 import type { Metadata } from "next";
deleted src/app/compte/CompteClient.tsx +0 −429
@@ -1,429 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −/**
3 − * Mon compte — carte de membre Groupe KA (gabarit « pc » de Lou·Ka transposé) :
4 − * fond encre, filigrane KA, KA-ID en gros mono lime (#d9f26b) avec glow,
5 − * porteur + avatar + badge statut, bande code-barres ; profil Groupe KA en
6 − * lecture seule (saisi sur groupe-ka.com/compte, LA source de vérité), grille
7 − * d'infos et actions dessous.
8 − * Le lime « Groupe KA » est propre à la carte (l'accent Vrai-Prix reste rouge).
9 − */
10 −"use client";
11 −import { useState } from "react";
12 −import { useRouter } from "next/navigation";
13 −import Link from "next/link";
14 −import { useLang } from "@/components/LangContext";
15 −import type { HubProfile, HubSocials } from "@/lib/hub-profile";
16 −
17 −// couleur signature du Groupe KA — réservée à la carte de membre
18 −const KA_LIME = "#d9f26b";
19 −
20 −type User = {
21 − ka_id: string;
22 − email: string;
23 − name: string;
24 − picture: string;
25 − provider: string;
26 −};
27 −
28 −// réseaux affichés en chips — libellé + URL depuis le pseudo (« @x ») ou l'URL
29 −const SOCIAL_LABELS: Record<keyof HubSocials, string> = {
30 − instagram: "Instagram",
31 − facebook: "Facebook",
32 − x: "X",
33 − linkedin: "LinkedIn",
34 − tiktok: "TikTok",
35 − youtube: "YouTube",
36 −};
37 −
38 −function socialUrl(net: keyof HubSocials, v: string): string {
39 − if (/^https?:\/\//i.test(v)) return v;
40 − const h = v.replace(/^@/, "");
41 − switch (net) {
42 − case "instagram":
43 − return `https://instagram.com/${h}`;
44 − case "facebook":
45 − return `https://facebook.com/${h}`;
46 − case "x":
47 − return `https://x.com/${h}`;
48 − case "linkedin":
49 − return `https://linkedin.com/in/${h}`;
50 − case "tiktok":
51 − return `https://tiktok.com/@${h}`;
52 − case "youtube":
53 − return `https://youtube.com/@${h}`;
54 − }
55 −}
56 −
57 −export default function CompteClient({
58 − user,
59 − profile,
60 −}: {
61 − user: User;
62 − profile: HubProfile | null;
63 −}) {
64 − const { lang } = useLang();
65 − const fr = lang === "fr";
66 − const router = useRouter();
67 − const [copied, setCopied] = useState(false);
68 − const [out, setOut] = useState(false);
69 −
70 − const copyKaId = async () => {
71 − try {
72 − await navigator.clipboard.writeText(user.ka_id);
73 − setCopied(true);
74 − setTimeout(() => setCopied(false), 1800);
75 − } catch {
76 − /* presse-papiers indisponible : tant pis */
77 − }
78 − };
79 −
80 − const logout = async () => {
81 − setOut(true);
82 − try {
83 − await fetch("/api/auth/logout", { method: "POST" });
84 − } finally {
85 − router.push("/");
86 − router.refresh();
87 − }
88 − };
89 −
90 − const infos: [string, React.ReactNode][] = [
91 − [fr ? "Nom" : "Name", user.name || "—"],
92 − [fr ? "Courriel" : "Email", user.email || "—"],
93 − [
94 − "KA-ID",
95 − <span key="k" className="vp-mono">{user.ka_id}</span>,
96 − ],
97 − [
98 − fr ? "Connexion" : "Sign-in",
99 − user.provider === "ka-id" || !user.provider
100 − ? "KA ID (groupe-ka.com)"
101 − : `KA ID · ${user.provider}`,
102 − ],
103 − ];
104 −
105 − return (
106 − <div className="mx-auto max-w-2xl pb-10 pt-10">
107 − <span className="kicker">{fr ? "Mon compte" : "My account"}</span>
108 − <h1 className="mb-7 mt-2 text-[clamp(30px,5vw,44px)] font-bold leading-tight">
109 − {user.name ? (
110 − <>
111 − {fr ? "Salut, " : "Hi, "}
112 − <span className="hl">{user.name.split(" ")[0]}</span>.
113 − </>
114 − ) : (
115 − <>
116 − {fr ? "Votre " : "Your "}
117 − <span className="hl">{fr ? "profil" : "profile"}</span>.
118 − </>
119 − )}
120 − </h1>
121 −
122 − {/* ——— Carte de membre Groupe KA ——— */}
123 − <div
124 − role="img"
125 − aria-label={`${fr ? "Carte de membre" : "Member card"} ${user.ka_id}`}
126 − className="relative max-w-[520px] overflow-hidden rounded-2xl border-2 border-ink bg-ink px-[26px] pt-6 text-paper"
127 − style={{ boxShadow: "10px 10px 0 rgba(20, 24, 20, 0.18)" }}
128 − >
129 − {/* filigrane */}
130 − <div
131 − aria-hidden="true"
132 − className="vp-display pointer-events-none absolute -top-[34px] right-[-18px] rotate-[-8deg] text-[170px] font-bold leading-none tracking-[-0.06em]"
133 − style={{ color: "rgba(217, 242, 107, 0.07)" }}
134 − >
135 − KA
136 − </div>
137 −
138 − {/* tête : marque + libellé */}
139 − <div className="flex flex-wrap items-baseline justify-between gap-3">
140 − <span className="vp-display text-[24px] font-bold tracking-[-0.04em]">
141 − Groupe
142 − <span
143 − className="ml-[3px] inline-block rotate-[-2deg] rounded-[5px] px-[6px] pb-[3px] pt-[1px] text-ink"
144 − style={{ background: KA_LIME }}
145 − >
146 − KA
147 − </span>
148 − </span>
149 − <span className="vp-mono text-[10px] uppercase tracking-[0.16em] text-[rgba(245,243,238,0.55)]">
150 − {fr ? "Carte de membre · Groupe KA" : "Member card · Groupe KA"}
151 − </span>
152 − </div>
153 −
154 − {/* KA-ID */}
155 − <div className="my-[24px] flex flex-col gap-1">
156 − <span
157 − className="vp-mono text-[10px] uppercase tracking-[0.2em]"
158 − style={{ color: "rgba(217, 242, 107, 0.65)" }}
159 − >
160 − KA-ID
161 − </span>
162 − <span
163 − className="vp-mono text-[clamp(22px,6vw,34px)] font-bold tracking-[0.14em]"
164 − style={{ color: KA_LIME, textShadow: "0 0 24px rgba(217, 242, 107, 0.35)" }}
165 − >
166 − {user.ka_id}
167 − </span>
168 − </div>
169 −
170 − {/* porteur + avatar */}
171 − <div className="flex items-end justify-between gap-3.5 pb-[18px]">
172 − <div className="flex min-w-0 flex-col gap-0.5">
173 − <span className="flex min-w-0 items-center gap-2">
174 − <span className="truncate text-[15px] font-semibold">
175 − {user.name || user.email}
176 − </span>
177 − {profile?.role_label ? (
178 − // badge statut — vient du hub (role_label), lime comme la carte
179 − <span
180 − className="vp-mono flex-none rounded-[4px] border px-[7px] pb-[2px] pt-[1px] text-[9.5px] font-bold uppercase tracking-[0.08em]"
181 − style={{
182 − borderColor: "rgba(217, 242, 107, 0.55)",
183 − color: KA_LIME,
184 − background: "rgba(217, 242, 107, 0.1)",
185 − }}
186 − >
187 − {profile.role_label}
188 − </span>
189 − ) : null}
190 − </span>
191 − <span className="vp-mono text-[10.5px] tracking-[0.06em] text-[rgba(245,243,238,0.5)]">
192 − {fr
193 − ? "Valide sur toutes les plateformes du Groupe KA"
194 − : "Valid across all Groupe KA platforms"}
195 − </span>
196 − </div>
197 − {user.picture ? (
198 − // eslint-disable-next-line @next/next/no-img-element
199 − <img
200 − src={user.picture}
201 − alt=""
202 − referrerPolicy="no-referrer"
203 − className="h-[52px] w-[52px] flex-none rounded-full border-2 object-cover"
204 − style={{ borderColor: KA_LIME }}
205 − />
206 − ) : (
207 − <span
208 − className="vp-display flex h-[52px] w-[52px] flex-none items-center justify-center rounded-full border-2 text-[22px] font-bold"
209 − style={{ borderColor: KA_LIME, color: KA_LIME }}
210 − >
211 − {(user.name || user.email || "K").charAt(0).toUpperCase()}
212 − </span>
213 − )}
214 − </div>
215 −
216 − {/* bande code-barres */}
217 − <div
218 − aria-hidden="true"
219 − className="-mx-[26px] flex gap-[5px] overflow-hidden px-[18px] py-[9px]"
220 − style={{
221 − background: "rgba(217, 242, 107, 0.1)",
222 − borderTop: "1px solid rgba(217, 242, 107, 0.25)",
223 − }}
224 − >
225 − {Array.from({ length: 34 }).map((_, i) => (
226 − <i
227 − key={i}
228 − className="block w-[3px] rounded-[1px]"
229 − style={{
230 − background: KA_LIME,
231 − height: i % 3 === 0 ? 14 : i % 3 === 1 ? 9 : 17,
232 − opacity: i % 3 === 0 ? 0.35 : i % 3 === 1 ? 0.7 : 0.9,
233 − }}
234 − />
235 − ))}
236 − </div>
237 − </div>
238 −
239 − {/* ——— actions carte ——— */}
240 − <div className="mt-4 flex flex-wrap gap-2.5">
241 − <button
242 − className="btn btn-ghost"
243 − onClick={copyKaId}
244 − style={copied ? { background: KA_LIME, color: "var(--ink)" } : undefined}
245 − >
246 − {copied ? "✓ " + (fr ? "Copié" : "Copied") : fr ? "Copier mon KA-ID" : "Copy my KA-ID"}
247 − </button>
248 − <Link href="/favoris" className="btn btn-ghost">
249 − ♥ {fr ? "Mes favoris" : "My favourites"}
250 − </Link>
251 − <button className="btn btn-ghost" onClick={logout} disabled={out}>
252 − {out ? (fr ? "Déconnexion…" : "Signing out…") : fr ? "Se déconnecter" : "Sign out"}
253 − </button>
254 − </div>
255 −
256 − {/* ——— Mon profil Groupe KA — lecture seule, saisi sur groupe-ka.com ——— */}
257 − <section className="vp-card mt-8 p-5 sm:p-6">
258 − <span className="kicker">
259 − {fr ? "Mon profil Groupe KA" : "My Groupe KA profile"}
260 − </span>
261 −
262 − {profile ? (
263 − <>
264 − {profile.bio ? (
265 − <p className="mt-3 max-w-xl text-[14.5px] leading-relaxed">
266 − {profile.bio}
267 − </p>
268 − ) : null}
269 −
270 − <div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
271 − {profile.job_title || profile.company ? (
272 − <div className="kv-cell">
273 − <div className="k">{fr ? "Emploi" : "Job"}</div>
274 − <div className="v break-words">
275 − {[profile.job_title, profile.company]
276 − .filter(Boolean)
277 − .join(" · ")}
278 − </div>
279 − </div>
280 − ) : null}
281 − {profile.city ? (
282 − <div className="kv-cell">
283 − <div className="k">{fr ? "Ville" : "City"}</div>
284 − <div className="v break-words">{profile.city}</div>
285 − </div>
286 − ) : null}
287 − {profile.age != null ? (
288 − <div className="kv-cell">
289 − <div className="k">{fr ? "Âge" : "Age"}</div>
290 − <div className="v">
291 − {profile.age} {fr ? "ans" : "yrs"}
292 − </div>
293 − </div>
294 − ) : null}
295 − {profile.website ? (
296 − <div className="kv-cell">
297 − <div className="k">{fr ? "Site web" : "Website"}</div>
298 − <div className="v break-words">
299 − <a
300 − href={
301 − /^https?:\/\//i.test(profile.website)
302 − ? profile.website
303 − : `https://${profile.website}`
304 − }
305 − target="_blank"
306 − rel="noopener noreferrer"
307 − className="underline decoration-2 underline-offset-2"
308 − >
309 − {profile.website.replace(/^https?:\/\//i, "")}
310 − </a>
311 − </div>
312 − </div>
313 − ) : null}
314 − <div className="kv-cell">
315 − <div className="k">{fr ? "Statut" : "Status"}</div>
316 − <div className="v break-words">
317 − {profile.role_label || (fr ? "Membre" : "Member")}
318 − {profile.public && profile.public_url ? (
319 − <>
320 − {" · "}
321 − <a
322 − href={profile.public_url}
323 − target="_blank"
324 − rel="noopener noreferrer"
325 − className="underline decoration-2 underline-offset-2"
326 − >
327 − {fr ? "profil public ↗" : "public profile ↗"}
328 − </a>
329 − </>
330 − ) : null}
331 − </div>
332 − </div>
333 − </div>
334 −
335 − {/* réseaux sociaux — chips cliquables */}
336 − {Object.entries(profile.socials || {}).some(([, v]) => v) ? (
337 − <div className="mt-4 flex flex-wrap gap-2">
338 − {(Object.keys(SOCIAL_LABELS) as (keyof HubSocials)[]).map(
339 − (net) => {
340 − const v = profile.socials?.[net];
341 − if (!v) return null;
342 − return (
343 − <a
344 − key={net}
345 − href={socialUrl(net, v)}
346 − target="_blank"
347 − rel="noopener noreferrer"
348 − className="stat-chip transition-colors hover:text-accent-deep"
349 − >
350 − <b>{SOCIAL_LABELS[net]}</b> {v}
351 − </a>
352 − );
353 − },
354 − )}
355 − </div>
356 − ) : null}
357 − </>
358 − ) : (
359 − <p className="mt-3 max-w-xl text-[13.5px] leading-relaxed text-ink-2">
360 − {fr
361 − ? "Votre profil se remplit sur le hub groupe-ka.com — bio, ville, emploi, réseaux sociaux…"
362 − : "Your profile is filled in on the groupe-ka.com hub — bio, city, job, social networks…"}
363 − </p>
364 − )}
365 −
366 − <div className="mt-5">
367 − <a
368 − href="https://www.groupe-ka.com/compte"
369 − target="_blank"
370 − rel="noopener noreferrer"
371 − className="btn btn-primary"
372 − >
373 − {fr
374 − ? "Modifier mon profil sur groupe-ka.com ↗"
375 − : "Edit my profile on groupe-ka.com ↗"}
376 − </a>
377 − </div>
378 − <p className="mt-3 text-[12.5px] text-ink-2">
379 − {fr
380 − ? "Une seule saisie, visible sur les huit plateformes du groupe."
381 − : "Enter it once — visible across the group's eight platforms."}
382 − </p>
383 − </section>
384 −
385 − {/* ——— informations ——— */}
386 − <section className="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2">
387 − {infos.map(([k, v]) => (
388 − <div key={k} className="kv-cell">
389 − <div className="k">{k}</div>
390 − <div className="v break-words">{v}</div>
391 − </div>
392 − ))}
393 − </section>
394 −
395 − <p className="mt-7 max-w-xl text-[13.5px] leading-relaxed text-ink-2">
396 − {fr ? (
397 − <>
398 − Votre <b>KA-ID</b> vous suit sur toutes les plateformes du{" "}
399 − <a
400 − href="https://www.groupe-ka.com"
401 − target="_blank"
402 − rel="noopener noreferrer"
403 − className="font-bold underline decoration-2 underline-offset-2"
404 − >
405 − Groupe KA
406 − </a>{" "}
407 − — c&apos;est le même identifiant partout. Vrai-Prix ne conserve que
408 − cette session (nom, courriel, photo) ; rien d&apos;autre, et jamais
409 − revendu.
410 − </>
411 − ) : (
412 − <>
413 − Your <b>KA-ID</b> follows you across every{" "}
414 − <a
415 − href="https://www.groupe-ka.com"
416 − target="_blank"
417 − rel="noopener noreferrer"
418 − className="font-bold underline decoration-2 underline-offset-2"
419 − >
420 − Groupe KA
421 − </a>{" "}
422 − platform — the same ID everywhere. Vrai-Prix only keeps this session
423 − (name, email, picture); nothing else, never sold.
424 − </>
425 − )}
426 − </p>
427 − </div>
428 − );
429 −}
deleted src/app/compte/page.tsx +0 −38
@@ -1,38 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Page « Mon compte » : carte de membre Groupe KA (KA-ID), profil saisi au hub
3 −// groupe-ka.com (bio, ville, emploi… — lecture seule ici), informations de la
4 −// session et actions. Sans session valide → départ SSO KA ID (retour ici).
5 −import type { Metadata } from "next";
6 −import { cookies } from "next/headers";
7 −import { redirect } from "next/navigation";
8 −import { SESSION_COOKIE, readSession } from "@/lib/ka-auth";
9 −import { fetchHubProfile } from "@/lib/hub-profile";
10 −import CompteClient from "./CompteClient";
11 −
12 −export const metadata: Metadata = {
13 − title: "Mon compte",
14 − description:
15 − "Votre carte de membre Groupe KA : le KA-ID vous suit sur toutes les plateformes du groupe.",
16 − robots: { index: false, follow: false },
17 −};
18 −
19 −export default async function ComptePage() {
20 − const jar = await cookies();
21 − const session = readSession(jar.get(SESSION_COOKIE)?.value);
22 − if (!session) redirect("/api/auth/ka/login?next=/compte");
23 − // Profil au hub Groupe KA — cache 60 s ; null si injoignable ou non relié
24 − // (on retombe alors sur les données de la session locale).
25 − const profile = await fetchHubProfile(session.ka_id);
26 − return (
27 − <CompteClient
28 − user={{
29 − ka_id: session.ka_id,
30 − email: profile?.email || session.email,
31 − name: profile?.name || session.name,
32 − picture: profile?.picture || session.picture,
33 − provider: session.provider ?? "ka-id",
34 − }}
35 − profile={profile}
36 − />
37 − );
38 −}
modified src/app/conditions/ConditionsClient.tsx +11 −11
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import LegalPage from "@/components/LegalPage";
4 4
@@ -6,14 +6,14 @@ const FR = [
6 6 {
7 7 title: "Objet et acceptation",
8 8 paras: [
9 − "Vrai-Prix (vrai-prix.com) est une plateforme d'estimation statistique de la valeur de propriétés situées au Québec, exploitée par Simon-Pierre Boucher (contact@spboucher.ai). En accédant au site ou en utilisant ses fonctionnalités — recherche d'adresse, estimation, rapport PDF — vous acceptez les présentes conditions d'utilisation. Si vous ne les acceptez pas, veuillez ne pas utiliser le service.",
9 + "UQO Éval (uqo-eval.app) est une plateforme d'estimation statistique de la valeur de propriétés situées au Québec, exploitée par Simon-Pierre Boucher (contact@spboucher.ai). En accédant au site ou en utilisant ses fonctionnalités — recherche d'adresse, estimation, rapport PDF — vous acceptez les présentes conditions d'utilisation. Si vous ne les acceptez pas, veuillez ne pas utiliser le service.",
10 10 ],
11 11 },
12 12 {
13 13 title: "Nature du service — pas une évaluation professionnelle",
14 14 paras: [
15 − "Les estimations produites par Vrai-Prix sont générées automatiquement par des modèles statistiques appliqués à des données publiques. Elles sont fournies à titre strictement informatif et indicatif.",
16 − "Une estimation Vrai-Prix ne constitue pas : une évaluation au sens de la Loi sur la fiscalité municipale ; un rapport d'évaluation préparé par un évaluateur agréé membre de l'Ordre des évaluateurs agréés du Québec (OEAQ) ; un avis de valeur marchande aux fins de financement hypothécaire, d'assurance, de litige, de partage ou de fiscalité ; ni un conseil financier, juridique ou immobilier. Pour toute décision importante, consultez un professionnel qualifié.",
15 + "Les estimations produites par UQO Éval sont générées automatiquement par des modèles statistiques appliqués à des données publiques. Elles sont fournies à titre strictement informatif et indicatif.",
16 + "Une estimation UQO Éval ne constitue pas : une évaluation au sens de la Loi sur la fiscalité municipale ; un rapport d'évaluation préparé par un évaluateur agréé membre de l'Ordre des évaluateurs agréés du Québec (OEAQ) ; un avis de valeur marchande aux fins de financement hypothécaire, d'assurance, de litige, de partage ou de fiscalité ; ni un conseil financier, juridique ou immobilier. Pour toute décision importante, consultez un professionnel qualifié.",
17 17 ],
18 18 },
19 19 {
@@ -25,7 +25,7 @@ const FR = [
25 25 {
26 26 title: "Sources de données",
27 27 paras: [
28 − "Le service s'appuie sur les rôles d'évaluation foncière du Québec diffusés en données ouvertes par le ministère des Affaires municipales et de l'Habitation (MAMH) sous licence ouverte, ainsi que sur des données de transactions publiées. Les renseignements relatifs aux propriétaires sont caviardés à la source par le MAMH et n'apparaissent nulle part sur Vrai-Prix. Malgré un soin raisonnable, les données sources peuvent contenir des erreurs, des retards de mise à jour ou des omissions.",
28 + "Le service s'appuie sur les rôles d'évaluation foncière du Québec diffusés en données ouvertes par le ministère des Affaires municipales et de l'Habitation (MAMH) sous licence ouverte, ainsi que sur des données de transactions publiées. Les renseignements relatifs aux propriétaires sont caviardés à la source par le MAMH et n'apparaissent nulle part sur UQO Éval. Malgré un soin raisonnable, les données sources peuvent contenir des erreurs, des retards de mise à jour ou des omissions.",
29 29 ],
30 30 },
31 31 {
@@ -37,7 +37,7 @@ const FR = [
37 37 {
38 38 title: "Propriété intellectuelle",
39 39 paras: [
40 − "La marque Vrai-Prix, l'interface, les modèles, les rapports PDF et la présentation des résultats sont la propriété de Simon-Pierre Boucher. Les données publiques sous-jacentes demeurent régies par leurs licences respectives (dont la licence des données ouvertes du gouvernement du Québec).",
40 + "La marque UQO Éval, l'interface, les modèles, les rapports PDF et la présentation des résultats sont la propriété de Simon-Pierre Boucher. Les données publiques sous-jacentes demeurent régies par leurs licences respectives (dont la licence des données ouvertes du gouvernement du Québec).",
41 41 ],
42 42 },
43 43 {
@@ -58,14 +58,14 @@ const EN = [
58 58 {
59 59 title: "Purpose and acceptance",
60 60 paras: [
61 − "Vrai-Prix (vrai-prix.com) is a statistical property-value estimation platform for Québec, operated by Simon-Pierre Boucher (contact@spboucher.ai). By accessing the site or using its features — address search, estimates, PDF reports — you accept these terms of service. If you do not accept them, please do not use the service.",
61 + "UQO Éval (uqo-eval.app) is a statistical property-value estimation platform for Québec, operated by Simon-Pierre Boucher (contact@spboucher.ai). By accessing the site or using its features — address search, estimates, PDF reports — you accept these terms of service. If you do not accept them, please do not use the service.",
62 62 ],
63 63 },
64 64 {
65 65 title: "Nature of the service — not a professional appraisal",
66 66 paras: [
67 − "Vrai-Prix estimates are generated automatically by statistical models applied to public data and are provided for information purposes only.",
68 − "A Vrai-Prix estimate is not: an assessment within the meaning of the Act respecting municipal taxation; an appraisal report prepared by a chartered appraiser (OEAQ); an opinion of market value for mortgage, insurance, litigation, division or tax purposes; nor financial, legal or real-estate advice. For any significant decision, consult a qualified professional.",
67 + "UQO Éval estimates are generated automatically by statistical models applied to public data and are provided for information purposes only.",
68 + "A UQO Éval estimate is not: an assessment within the meaning of the Act respecting municipal taxation; an appraisal report prepared by a chartered appraiser (OEAQ); an opinion of market value for mortgage, insurance, litigation, division or tax purposes; nor financial, legal or real-estate advice. For any significant decision, consult a qualified professional.",
69 69 ],
70 70 },
71 71 {
@@ -77,7 +77,7 @@ const EN = [
77 77 {
78 78 title: "Data sources",
79 79 paras: [
80 − "The service relies on Québec's property assessment rolls released as open data by the MAMH under an open licence, and on published transaction data. Owner information is redacted at the source by the MAMH and appears nowhere on Vrai-Prix. Despite reasonable care, source data may contain errors, update delays or omissions.",
80 + "The service relies on Québec's property assessment rolls released as open data by the MAMH under an open licence, and on published transaction data. Owner information is redacted at the source by the MAMH and appears nowhere on UQO Éval. Despite reasonable care, source data may contain errors, update delays or omissions.",
81 81 ],
82 82 },
83 83 {
@@ -89,7 +89,7 @@ const EN = [
89 89 {
90 90 title: "Intellectual property",
91 91 paras: [
92 − "The Vrai-Prix brand, interface, models, PDF reports and presentation of results are the property of Simon-Pierre Boucher. The underlying public data remains governed by its respective licences (including the Québec government open-data licence).",
92 + "The UQO Éval brand, interface, models, PDF reports and presentation of results are the property of Simon-Pierre Boucher. The underlying public data remains governed by its respective licences (including the Québec government open-data licence).",
93 93 ],
94 94 },
95 95 {
modified src/app/conditions/page.tsx +2 −2
@@ -1,11 +1,11 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata } from "next";
3 3 import ConditionsClient from "./ConditionsClient";
4 4
5 5 export const metadata: Metadata = {
6 6 title: "Conditions d'utilisation",
7 7 description:
8 − "Conditions d'utilisation de Vrai-Prix : nature du service d'estimation statistique, exactitude et fourchettes, propriété intellectuelle, limites de responsabilité.",
8 + "Conditions d'utilisation de UQO Éval : nature du service d'estimation statistique, exactitude et fourchettes, propriété intellectuelle, limites de responsabilité.",
9 9 alternates: { canonical: "/conditions" },
10 10 };
11 11
modified src/app/confidentialite/ConfidentialiteClient.tsx +5 −5
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import LegalPage from "@/components/LegalPage";
4 4 import { openCookiePrefs } from "@/components/CookieConsent";
@@ -8,7 +8,7 @@ const FR = [
8 8 {
9 9 title: "Qui est responsable de vos renseignements",
10 10 paras: [
11 − "La présente politique décrit comment Vrai-Prix (vrai-prix.com) recueille, utilise et protège les renseignements personnels, conformément à la Loi sur la protection des renseignements personnels dans le secteur privé du Québec (telle que modifiée par la Loi 25). Le responsable de la protection des renseignements personnels est Simon-Pierre Boucher, joignable à contact@spboucher.ai.",
11 + "La présente politique décrit comment UQO Éval (uqo-eval.app) recueille, utilise et protège les renseignements personnels, conformément à la Loi sur la protection des renseignements personnels dans le secteur privé du Québec (telle que modifiée par la Loi 25). Le responsable de la protection des renseignements personnels est Simon-Pierre Boucher, joignable à contact@spboucher.ai.",
12 12 ],
13 13 },
14 14 {
@@ -21,7 +21,7 @@ const FR = [
21 21 {
22 22 title: "Finalités et conservation",
23 23 paras: [
24 − "Le courriel fourni sert exclusivement à vous transmettre le rapport demandé et, si vous y consentez explicitement, des suivis liés à Vrai-Prix. Il est conservé au maximum 24 mois après la dernière interaction, puis supprimé. Les journaux techniques sont conservés au plus 90 jours. Les données sont hébergées sur une infrastructure privée située au Canada (Québec).",
24 + "Le courriel fourni sert exclusivement à vous transmettre le rapport demandé et, si vous y consentez explicitement, des suivis liés à UQO Éval. Il est conservé au maximum 24 mois après la dernière interaction, puis supprimé. Les journaux techniques sont conservés au plus 90 jours. Les données sont hébergées sur une infrastructure privée située au Canada (Québec).",
25 25 ],
26 26 },
27 27 {
@@ -57,7 +57,7 @@ const EN = [
57 57 {
58 58 title: "Who is responsible for your information",
59 59 paras: [
60 − "This policy describes how Vrai-Prix (vrai-prix.com) collects, uses and protects personal information, in accordance with Québec's Act respecting the protection of personal information in the private sector (as amended by Law 25). The person in charge of the protection of personal information is Simon-Pierre Boucher, reachable at contact@spboucher.ai.",
60 + "This policy describes how UQO Éval (uqo-eval.app) collects, uses and protects personal information, in accordance with Québec's Act respecting the protection of personal information in the private sector (as amended by Law 25). The person in charge of the protection of personal information is Simon-Pierre Boucher, reachable at contact@spboucher.ai.",
61 61 ],
62 62 },
63 63 {
@@ -70,7 +70,7 @@ const EN = [
70 70 {
71 71 title: "Purposes and retention",
72 72 paras: [
73 − "The email you provide is used exclusively to send you the requested report and, with your explicit consent, Vrai-Prix follow-ups. It is kept at most 24 months after the last interaction, then deleted. Technical logs are kept at most 90 days. Data is hosted on private infrastructure located in Canada (Québec).",
73 + "The email you provide is used exclusively to send you the requested report and, with your explicit consent, UQO Éval follow-ups. It is kept at most 24 months after the last interaction, then deleted. Technical logs are kept at most 90 days. Data is hosted on private infrastructure located in Canada (Québec).",
74 74 ],
75 75 },
76 76 {
modified src/app/confidentialite/page.tsx +2 −2
@@ -1,11 +1,11 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata } from "next";
3 3 import ConfidentialiteClient from "./ConfidentialiteClient";
4 4
5 5 export const metadata: Metadata = {
6 6 title: "Politique de confidentialité (Loi 25)",
7 7 description:
8 − "Politique de confidentialité de Vrai-Prix : renseignements recueillis, finalités, conservation, hébergement au Québec et droits prévus par la Loi 25.",
8 + "Politique de confidentialité de UQO Éval : renseignements recueillis, finalités, conservation, hébergement au Québec et droits prévus par la Loi 25.",
9 9 alternates: { canonical: "/confidentialite" },
10 10 };
11 11
modified src/app/contact/page.tsx +47 −68
@@ -1,97 +1,76 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Page contact — infos centralisées de l'écosystème Groupe KA (ka-ui/ecosystem.json).
1 +// UQO Éval — page contact (cadre pédagogique universitaire).
3 2 import type { Metadata } from "next";
4 −import eco from "@/ka/ecosystem.json";
5 3
6 4 export const metadata: Metadata = {
7 5 title: "Contact",
8 6 description:
9 − "Joindre le Groupe KA : projets et partenariats, médias, légal et vie privée. Vrai-Prix est un service Groupe KA.",
7 + "Joindre l'équipe d'UQO Éval — laboratoire pédagogique d'évaluation immobilière (cours IMM1003 et IMM1033, UQO).",
10 8 };
11 9
12 −const SELF_ID = "vrai-prix";
13 −
14 10 export default function ContactPage() {
15 − const others = eco.sites.filter((s) => s.id !== SELF_ID);
16 11 return (
17 12 <div className="pb-6 pt-10">
18 13 <p className="kicker">Contact</p>
19 14 <h1 className="vp-display mt-3 max-w-3xl text-[clamp(30px,5vw,52px)] font-bold uppercase leading-[1] tracking-[-0.03em]">
20 − Écrire au <span className="hl">Groupe&nbsp;KA</span>
15 + Écrire à <span className="hl">UQO&nbsp;Éval</span>
21 16 </h1>
22 17 <p className="mt-5 max-w-2xl text-[15px] leading-relaxed text-ink-2">
23 − Vrai-Prix est un service du <b className="text-ink">{eco.org.name}</b> —{" "}
24 − {eco.org.tagline} Toutes les demandes passent par les adresses
25 − centralisées ci-dessous.
18 + UQO Éval est un <b className="text-ink">laboratoire pédagogique
19 + d&apos;évaluation immobilière</b> développé dans le cadre des cours{" "}
20 + <b className="text-ink">IMM1003 — Éléments d&apos;évaluation
21 + immobilière</b> et <b className="text-ink">IMM1033 — Méthodes du coût en
22 + évaluation immobilière</b>, enseignés par Simon-Pierre Boucher à
23 + l&apos;Université du Québec en Outaouais.
26 24 </p>
27 25
28 − {/* les 3 courriels avec leurs rôles */}
29 − <div className="mt-9 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
30 − {eco.contacts.map((c) => (
31 − <a
32 − key={c.email}
33 − href={`mailto:${c.email}`}
34 − className="card card-hover block p-5"
35 − >
36 − <p className="klabel">{c.role}</p>
37 − <p className="vp-mono mt-2 break-all text-[13.5px] font-bold text-ink">
38 − {c.email}
39 − </p>
40 − </a>
41 − ))}
26 + <div className="mt-9 grid max-w-3xl gap-5 sm:grid-cols-2">
27 + <div className="border-t-2 border-ink pt-4">
28 + <p className="klabel">Enseignant responsable</p>
29 + <p className="vp-display mt-2 text-[17px] font-bold">Simon-Pierre Boucher</p>
30 + <p className="mt-1 text-[13.5px] text-ink-2">
31 + Université du Québec en Outaouais — cours IMM1003 · IMM1033
32 + </p>
33 + </div>
34 + <div className="border-t-2 border-ink pt-4">
35 + <p className="klabel">Université</p>
36 + <p className="vp-display mt-2 text-[17px] font-bold">
37 + <a
38 + href="https://uqo.ca"
39 + target="_blank"
40 + rel="noopener noreferrer"
41 + className="border-b-2 border-accent hover:text-accent-deep"
42 + >
43 + uqo.ca
44 + </a>
45 + </p>
46 + <p className="mt-1 text-[13.5px] text-ink-2">
47 + Université du Québec en Outaouais — Gatineau · Saint-Jérôme
48 + </p>
49 + </div>
42 50 </div>
43 51
44 − {/* avertissement d'agrégateur */}
45 − <div className="card mt-8 max-w-2xl border-l-[6px] border-l-lime p-5">
52 + {/* avertissement pédagogique */}
53 + <div className="mt-10 max-w-2xl border-l-2 border-accent pl-5">
46 54 <p className="klabel">Avertissement</p>
47 − <p className="mt-2 text-[14px] leading-relaxed">{eco.org.disclaimer}</p>
55 + <p className="mt-2 text-[14px] leading-relaxed text-ink-2">
56 + Plateforme à vocation pédagogique, universitaire et expérimentale. Les
57 + estimations produites sont statistiques et ne constituent pas une
58 + évaluation immobilière professionnelle certifiée au sens des normes de
59 + l&apos;Ordre des évaluateurs agréés du Québec (OEAQ).
60 + </p>
48 61 </div>
49 62
50 − {/* hub + compte KA ID */}
51 63 <div className="mt-10">
52 − <p className="kicker">Le hub</p>
64 + <p className="kicker">Données</p>
53 65 <p className="mt-3 max-w-2xl text-[14.5px] text-ink-2">
54 − Le portail de l&apos;écosystème vit sur{" "}
55 − <a
56 − href={eco.hub.url}
57 − target="_blank"
58 − rel="noopener noreferrer"
59 − className="font-bold text-ink underline decoration-lime decoration-2 underline-offset-4"
60 − >
61 − groupe-ka.com
66 + Rôles d&apos;évaluation foncière géoréférencés du Québec (MAMH, données
67 + ouvertes) et transactions immobilières publiques — voir la{" "}
68 + <a href="/methodologie" className="font-bold text-ink underline decoration-accent decoration-2 underline-offset-4">
69 + méthodologie complète
62 70 </a>
63 − . La création d&apos;un compte KA&nbsp;ID se fait sur{" "}
64 − <a
65 − href={`${eco.hub.url}${eco.hub.loginPath}`}
66 − target="_blank"
67 − rel="noopener noreferrer"
68 − className="font-bold text-ink underline decoration-lime decoration-2 underline-offset-4"
69 − >
70 − groupe-ka.com/connexion
71 − </a>{" "}
72 − — le même compte ouvre toutes les plateformes du groupe.
71 + .
73 72 </p>
74 73 </div>
75 −
76 − {/* les sites de l'écosystème */}
77 − <div className="mt-10">
78 − <p className="kicker">L&apos;écosystème</p>
79 − <ul className="mt-4 flex max-w-3xl list-none flex-wrap gap-2.5 p-0">
80 − {others.map((s) => (
81 − <li key={s.id}>
82 − <a
83 − href={`https://${s.domain}`}
84 − target="_blank"
85 − rel="noopener noreferrer"
86 − className="chip"
87 − title={s.tagline}
88 − >
89 − {s.wordmark}
90 − </a>
91 − </li>
92 − ))}
93 − </ul>
94 − </div>
95 74 </div>
96 75 );
97 76 }
modified src/app/estimation/[id]/page.tsx +2 −2
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata } from "next";
3 3 import { cache } from "react";
4 4 import { notFound } from "next/navigation";
@@ -81,7 +81,7 @@ function unitJsonLd(data: NonNullable<ReturnType<typeof estimateByUnitId>>) {
81 81 additionalProperty: [
82 82 {
83 83 "@type": "PropertyValue",
84 − name: `Estimation Vrai-Prix ${EST_YEAR}`,
84 + name: `Estimation UQO Éval ${EST_YEAR}`,
85 85 value: r.estimate,
86 86 unitText: "CAD",
87 87 },
modified src/app/estimation/manuelle/page.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata } from "next";
3 3 import { notFound } from "next/navigation";
4 4 import { estimateManual } from "@/lib/estimator";
modified src/app/estimation/sitemap.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Sitemaps des 3,7 M de fiches /estimation/[id], découpés par plages de rowid
3 3 // (contigus 1..N dans la DB). Servis à /estimation/sitemap/[id].xml.
4 4 import type { MetadataRoute } from "next";
deleted src/app/favoris/FavorisClient.tsx +0 −158
@@ -1,158 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −/**
3 − * « Mes favoris » — liste des propriétés mises en ♥ (Mon univers Ka).
4 − * Le hub Groupe KA est le magasin central : retirer un favori = POST synchrone
5 − * /api/favorites/toggle (action « remove »), la rangée disparaît si le hub
6 − * confirme. Design éditorial Vrai-Prix (papier/encre, cartes à ombre franche).
7 − */
8 −"use client";
9 −import { useState } from "react";
10 −import Link from "next/link";
11 −import { useLang } from "@/components/LangContext";
12 −import type { FavItem } from "@/lib/hubfav";
13 −
14 −/** Lien local vers la fiche : chemin du hub si c'est une URL Vrai-Prix,
15 − * sinon retombée sur /estimation/<item_id>. */
16 −function ficheHref(f: FavItem): string {
17 − if (f.url) {
18 − try {
19 − const u = new URL(f.url);
20 − if (u.hostname.endsWith("vrai-prix.com")) return u.pathname + u.search;
21 − } catch {
22 − /* URL invalide — retombée ci-dessous */
23 − }
24 − }
25 − return `/estimation/${encodeURIComponent(f.item_id)}`;
26 −}
27 −
28 −export default function FavorisClient({ initial }: { initial: FavItem[] | null }) {
29 − const { lang } = useLang();
30 − const fr = lang === "fr";
31 − const [items, setItems] = useState<FavItem[] | null>(initial);
32 − const [busy, setBusy] = useState<string | null>(null);
33 − const [err, setErr] = useState(false);
34 −
35 − const remove = async (f: FavItem) => {
36 − if (busy) return;
37 − setBusy(f.item_id);
38 − setErr(false);
39 − try {
40 − const r = await fetch("/api/favorites/toggle", {
41 − method: "POST",
42 − headers: { "Content-Type": "application/json" },
43 − body: JSON.stringify({ ...f, action: "remove" }),
44 − });
45 − if (!r.ok) throw new Error(String(r.status));
46 − setItems((cur) => (cur ?? []).filter((x) => x.item_id !== f.item_id));
47 − } catch {
48 − setErr(true);
49 − } finally {
50 − setBusy(null);
51 − }
52 − };
53 −
54 − return (
55 − <div className="py-10">
56 − {/* en-tête */}
57 − <span className="kicker">{fr ? "Mon univers Ka" : "My Ka universe"}</span>
58 − <h1 className="vp-display mt-2 text-[clamp(28px,5vw,44px)] font-bold uppercase leading-none tracking-[-0.03em]">
59 − {fr ? "Mes favoris" : "My favourites"}
60 − </h1>
61 − <p className="mt-3 max-w-2xl text-[15px] leading-relaxed text-ink-2">
62 − {fr
63 − ? "Les propriétés que vous avez mises en ♥ — gardées au hub Groupe KA, les mêmes sur toutes les plateformes du groupe."
64 − : "The properties you hearted — stored at the Groupe KA hub, the same across every platform of the group."}
65 − </p>
66 −
67 − {err && (
68 − <p className="vp-mono mt-4 rounded-[8px] border-[1.5px] border-red-600 bg-surface px-4 py-3 text-[12px] uppercase tracking-[0.04em] text-red-700">
69 − {fr
70 − ? "Hub Groupe KA injoignable — le retrait n'a pas été enregistré, réessayez."
71 − : "Groupe KA hub unreachable — removal was not saved, try again."}
72 − </p>
73 − )}
74 −
75 − {/* hub injoignable au chargement */}
76 − {items === null ? (
77 − <div className="vp-card mt-8 p-6 sm:p-8">
78 − <p className="vp-display text-[18px] font-bold">
79 − {fr ? "Hub Groupe KA injoignable" : "Groupe KA hub unreachable"}
80 − </p>
81 − <p className="mt-2 text-[14.5px] text-ink-2">
82 − {fr
83 − ? "Vos favoris vivent sur groupe-ka.com et n'ont pas pu être lus. Rechargez la page dans un instant."
84 − : "Your favourites live on groupe-ka.com and could not be read. Reload the page in a moment."}
85 − </p>
86 − </div>
87 − ) : items.length === 0 ? (
88 − /* aucun favori */
89 − <div className="vp-card mt-8 p-6 sm:p-8">
90 − <p className="vp-display text-[18px] font-bold">
91 − {fr ? "Aucun favori pour l'instant" : "No favourites yet"}
92 − </p>
93 − <p className="mt-2 max-w-xl text-[14.5px] text-ink-2">
94 − {fr
95 − ? "Touchez le ♥ d'une fiche d'estimation pour la garder ici — elle vous suivra sur toutes les plateformes du Groupe KA."
96 − : "Tap the ♥ on any estimate page to keep it here — it follows you across every Groupe KA platform."}
97 − </p>
98 − <Link href="/" className="btn btn-primary vp-display mt-5 inline-flex">
99 − {fr ? "Estimer une propriété" : "Estimate a property"}
100 − </Link>
101 − </div>
102 − ) : (
103 − <>
104 − <p className="vp-mono mt-8 text-[11px] font-bold uppercase tracking-[0.08em] text-ink-3">
105 − {items.length}{" "}
106 − {fr
107 − ? `propriété${items.length > 1 ? "s" : ""} en favoris`
108 − : `favourite propert${items.length > 1 ? "ies" : "y"}`}
109 − </p>
110 − <ul className="mt-3 grid gap-4 sm:grid-cols-2">
111 − {items.map((f, i) => (
112 − <li
113 − key={f.item_id}
114 − className="vp-card vp-card-hover rise relative p-5"
115 − style={{ animationDelay: `${i * 60}ms` }}
116 − >
117 − <Link href={ficheHref(f)} className="block pr-10">
118 − <p className="vp-mono text-[10px] font-bold text-green">
119 − {String(i + 1).padStart(2, "0")}
120 − </p>
121 − <p className="vp-display mt-0.5 text-[16px] font-bold uppercase leading-tight">
122 − {f.title}
123 − </p>
124 − {f.subtitle && (
125 − <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.05em] text-ink-3">
126 − {f.subtitle}
127 − </p>
128 − )}
129 − <div className="mt-3 flex items-end justify-between gap-3 border-t-[1.5px] border-dashed border-[rgba(20,24,20,0.14)] pt-3">
130 − <p className="vp-display text-[20px] font-bold tracking-[-0.02em]">
131 − {f.price_label || "—"}
132 − </p>
133 − <span className="vp-mono text-[11px] font-bold uppercase tracking-[0.04em] text-green">
134 − {fr ? "Voir la fiche →" : "View →"}
135 − </span>
136 − </div>
137 − </Link>
138 − {/* retirer (♥ plein — même geste que sur la fiche) */}
139 − <button
140 − type="button"
141 − onClick={() => remove(f)}
142 − disabled={busy === f.item_id}
143 − aria-label={fr ? "Retirer de mes favoris" : "Remove from my favourites"}
144 − title={fr ? "Retirer de mes favoris" : "Remove from my favourites"}
145 − className={`absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-full border-[1.5px] border-ink bg-ink text-[14px] leading-none text-lime transition-all hover:bg-green-deep ${
146 − busy === f.item_id ? "opacity-60" : ""
147 − }`}
148 − >
149 − <span aria-hidden="true" className="translate-y-[-1px]">♥</span>
150 − </button>
151 − </li>
152 − ))}
153 − </ul>
154 − </>
155 − )}
156 − </div>
157 − );
158 −}
deleted src/app/favoris/page.tsx +0 −28
@@ -1,28 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Page « Mes favoris » — Mon univers Ka : les propriétés mises en ♥, lues au
3 −// hub Groupe KA (magasin central des favoris du groupe — rien en local).
4 −// Sans session KA ID valide → départ SSO (retour ici).
5 −import type { Metadata } from "next";
6 −import { cookies } from "next/headers";
7 −import { redirect } from "next/navigation";
8 −import { SESSION_COOKIE, readSession } from "@/lib/ka-auth";
9 −import { hubList } from "@/lib/hubfav";
10 −import FavorisClient from "./FavorisClient";
11 −
12 −export const dynamic = "force-dynamic";
13 −
14 −export const metadata: Metadata = {
15 − title: "Mes favoris",
16 − description:
17 − "Mon univers Ka : vos propriétés favorites, les mêmes sur toutes les plateformes du Groupe KA.",
18 − robots: { index: false, follow: false },
19 −};
20 −
21 −export default async function FavorisPage() {
22 − const jar = await cookies();
23 − const session = readSession(jar.get(SESSION_COOKIE)?.value);
24 − if (!session) redirect("/api/auth/ka/login?next=/favoris");
25 − // Liste au hub Groupe KA (cache 30 s) — null si le hub est injoignable.
26 − const favorites = await hubList(session.ka_id);
27 − return <FavorisClient initial={favorites} />;
28 −}
modified src/app/globals.css +37 −36
@@ -1,14 +1,15 @@
1 1 /* -----------------------------------------------------------------------------
2 − Vrai-Prix — estimation immobilière transparente (Québec)
3 − Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 − Système de design « terminal éditorial » v2 — Bloomberg × FT data × institutionnel :
2 + UQO Éval — laboratoire pédagogique d'évaluation immobilière
3 + Système de design « terminal éditorial » v2, ré-habillé aux couleurs
4 + officielles de l'UQO (Guide de normes graphiques, déc. 2025) :
5 + · Bleu institutionnel PMS 7469 (#006085) = couleur signal
6 + · Turquoise web UQO (#2BCCAE) = marqueurs sur surfaces encre
5 7 · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
6 − · Signature : filets fins (hairlines), grands chiffres, compositions asymétriques,
7 − encre profonde + rouge signal réservé à l'information importante
8 − · Zéro ombre décalée, zéro pilule, radius 0-2 px — la donnée fait le décor
8 + · Signature : filets fins (hairlines), grands chiffres, compositions
9 + asymétriques — la donnée fait le décor
9 10 ----------------------------------------------------------------------------- */
10 11 @import "tailwindcss";
11 −/* ka-ui (Groupe KA) — tokens + composants communs (.gk-badge, .ka-footer, .card…).
12 +/* tokens + socle de composants communs (.card, .ka-menu, socle mobile…).
12 13 Importé dans layer(base) AVANT le corps de ce fichier : il bat le preflight
13 14 Tailwind mais reste sous les utilitaires (layered) et sous nos règles locales
14 15 (non-layered). Un import brut dans layout.tsx écraserait les text-[…]/leading-[…]
@@ -16,44 +17,44 @@
16 17 @import "../ka/tokens.css" layer(base);
17 18
18 19 :root {
19 − /* ---- Accent Vrai-Prix (tokens canoniques ka-ui) — identité rouge ---- */
20 − --accent: #d8372e; /* rouge signal éditorial (lisible sur papier) */
21 − --accent-bright: #ff5148; /* rouge vif — marqueurs, points de donnée sur encre */
22 − --accent-soft: #ffe3e0;
23 − --accent-deep: #9e2a25;
20 + /* ---- Accent UQO Éval — bleu institutionnel UQO (PMS 7469) ---- */
21 + --accent: #006085; /* bleu UQO — signal éditorial (lisible sur papier) */
22 + --accent-bright: #2bccae; /* turquoise UQO — marqueurs, points de donnée sur encre */
23 + --accent-soft: #dcebf2;
24 + --accent-deep: #00465f;
24 25 --on-accent: #ffffff;
25 26
26 − --paper: #f5f3ee;
27 + --paper: #f4f5f3;
27 28 --surface: #ffffff;
28 − --surface-2: #faf9f5;
29 − --ink: #141814;
30 − --ink-2: #4d5551;
31 − --ink-3: #8b928c;
32 − --line: rgba(20, 24, 20, 0.14);
33 − --line-soft: rgba(20, 24, 20, 0.08);
34 − --line-strong: rgba(20, 24, 20, 0.85);
35 − --green: #9e2a25;
36 − --green-deep: #771f1b;
29 + --surface-2: #f9fafa;
30 + --ink: #16181b;
31 + --ink-2: #4b5258;
32 + --ink-3: #8b9297;
33 + --line: rgba(22, 24, 27, 0.14);
34 + --line-soft: rgba(22, 24, 27, 0.08);
35 + --line-strong: rgba(22, 24, 27, 0.85);
36 + --green: #00465f;
37 + --green-deep: #003448;
37 38 --lime: var(--accent); /* alias historiques → l'accent est le token canonique */
38 39 --lime-soft: var(--accent-soft);
39 40 --amber: #e8a33d;
40 41 --amber-soft: #fdf3e2;
41 42 --danger: #b3423a;
42 43 --danger-soft: #fbe9e7;
43 − /* série de graphiques (palette validée : CVD 28,6 · contraste ≥3:1) */
44 − --chart-1: #141814; /* unifamiliale */
45 − --chart-2: #d8372e; /* condo */
46 − --chart-3: #6b736d; /* plex */
44 + /* série de graphiques (encre · bleu UQO · corail UQO — hues bien séparées) */
45 + --chart-1: #16181b; /* unifamiliale */
46 + --chart-2: #006085; /* condo */
47 + --chart-3: #ea6047; /* plex */
47 48 --r-card: 2px;
48 49 --r-ctl: 2px;
49 − --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05);
50 + --shadow-flat: 0 1px 2px rgba(22, 24, 27, 0.05);
50 51 /* ombres décalées héritées — aplaties : le v2 ne porte aucune ombre dure */
51 − --shadow-off: 0 1px 2px rgba(20, 24, 20, 0.08);
52 + --shadow-off: 0 1px 2px rgba(22, 24, 27, 0.08);
52 53 --shadow-off-soft: none;
53 − --shadow-off-mid: 0 1px 2px rgba(20, 24, 20, 0.08);
54 − /* encre légèrement plus profonde pour les aplats pleine largeur */
55 − --ink-deep: #101410;
56 − --paper-on-ink: #edeae2;
54 + --shadow-off-mid: 0 1px 2px rgba(22, 24, 27, 0.08);
55 + /* encre légèrement plus profonde pour les aplats pleine largeur (bleu nuit UQO) */
56 + --ink-deep: #0d1b24;
57 + --paper-on-ink: #e9edee;
57 58 }
58 59
59 60 @theme inline {
@@ -211,14 +212,14 @@ select.vp-input {
211 212 width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
212 213 align-self: center; animation: pulse 2.4s ease infinite;
213 214 }
214 −@keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(216, 55, 46, 0.18); } }
215 +@keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(0, 96, 133, 0.18); } }
215 216
216 217 /* surligné signature : marqueur rouge sous le texte (plus de boîte inclinée).
217 218 ⚠️ `color: inherit` est OBLIGATOIRE : le .hl de ka-ui (tokens.css) impose
218 219 color: var(--on-accent) = blanc — illisible sur papier. Ici le texte garde
219 220 la couleur du titre (encre sur papier, off-white sur .vp-dark). */
220 221 .hl {
221 − background: linear-gradient(transparent 62%, rgba(216, 55, 46, 0.32) 62%);
222 + background: linear-gradient(transparent 62%, rgba(43, 204, 174, 0.45) 62%);
222 223 color: inherit;
223 224 padding: 0 2px; border-radius: 0; display: inline; transform: none;
224 225 }
@@ -343,7 +344,7 @@ select.vp-input {
343 344 .rise, .grow-x, .vv-win { animation: none; }
344 345 }
345 346
346 −/* barres horizontales (historique) — fines, encre, millésime courant en rouge */
347 +/* barres horizontales (historique) — fines, encre, millésime courant en bleu UQO */
347 348 .hbar-track { background: var(--line-soft); border-radius: 0; height: 12px; overflow: hidden; }
348 349 .hbar-fill {
349 350 display: block; height: 100%; background: var(--ink); border-radius: 0;
@@ -352,7 +353,7 @@ select.vp-input {
352 353 .hbar-row:hover .hbar-fill { background: var(--accent); }
353 354
354 355
355 −/* Vrai-Prix Maps (framework Ka Maps par Groupe Ka) — recherche par carte */
356 +/* UQO Éval Maps — recherche par carte */
356 357 .vpm-wrap {
357 358 position: relative;
358 359 height: 62vh;
deleted src/app/ka/page.tsx +0 −14
@@ -1,14 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −import type { Metadata } from "next";
3 −import KaHero from "./KaHero";
4 −
5 −export const metadata: Metadata = {
6 − title: "Ka — l'agent IA qui évalue n'importe quelle propriété du Québec",
7 − description:
8 − "Ka évalue n'importe quelle propriété du Québec en conversant : recherche dans le registre, évaluation transparente, comparables, insights de marché — en direct.",
9 − alternates: { canonical: "/ka" },
10 −};
11 −
12 −export default function KaPage() {
13 − return <KaHero />;
14 −}
modified src/app/layout.tsx +54 −55
@@ -1,16 +1,14 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata, Viewport } from "next";
3 3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google";
4 4 import "./globals.css";
5 5 import "../ka/ka-tabbar.css";
6 6 import { LangProvider, LangToggle } from "@/components/LangContext";
7 −import AccountButton from "@/components/AccountButton";
8 7 import CookieConsent from "@/components/CookieConsent";
9 8 import FooterNav from "@/components/FooterNav";
10 9 import MobileTabBar from "@/components/MobileTabBar";
11 10 import HeaderNav from "@/components/HeaderNav";
12 −import GroupeKaBadge from "@/ka/GroupeKaBadge";
13 −import KaFooter from "@/ka/KaFooter";
11 +import UqoFooter from "@/components/UqoFooter";
14 12 import Link from "next/link";
15 13 import { SITE_URL } from "@/lib/seo";
16 14
@@ -27,33 +25,30 @@ const jetbrains = JetBrains_Mono({
27 25 export const metadata: Metadata = {
28 26 metadataBase: new URL(SITE_URL),
29 27 title: {
30 − default: "Vrai-Prix — Un service Groupe KA",
31 − template: "%s | Vrai-Prix",
28 + default: "UQO Éval — Laboratoire pédagogique d'évaluation immobilière",
29 + template: "%s | UQO Éval",
32 30 },
33 31 description:
34 − "Estimation immobilière transparente pour le Québec : modèle hédonique + comparables réels, fourchette honnête et indice de confiance. 3,7 millions de propriétés, 1 100 municipalités.",
35 − applicationName: "Vrai-Prix",
32 + "Plateforme universitaire pour comprendre comment une propriété est évaluée : modèle hédonique, comparables réels, ajustements montrés en dollars, intervalle de confiance. 3,7 millions de propriétés du Québec, 1 100 municipalités.",
33 + applicationName: "UQO Éval",
34 + manifest: "/manifest.webmanifest",
36 35 icons: { icon: "/favicon.svg", apple: "/apple-touch-icon.png" },
37 36 openGraph: {
38 37 type: "website",
39 − siteName: "Vrai-Prix",
38 + siteName: "UQO Éval",
40 39 locale: "fr_CA",
41 40 url: SITE_URL,
42 − title: "Vrai-Prix — Un service Groupe KA",
41 + title: "UQO Éval — Laboratoire pédagogique d'évaluation immobilière",
43 42 description:
44 − "Estimation immobilière transparente pour le Québec : modèle hédonique + comparables réels, fourchette honnête et indice de confiance.",
45 − images: [
46 − { url: "https://www.vrai-prix.com/og.png", width: 1200, height: 630 },
47 − ],
43 + "Comprendre comment une propriété est évaluée : modèle hédonique, comparables réels, ajustements en dollars, intervalle de confiance — le calcul montré en entier.",
44 + images: [{ url: `${SITE_URL}/og.png`, width: 1200, height: 630 }],
48 45 },
49 46 twitter: {
50 47 card: "summary_large_image",
51 − images: [
52 − { url: "https://www.vrai-prix.com/og.png", width: 1200, height: 630 },
53 − ],
54 − title: "Vrai-Prix — Un service Groupe KA",
48 + images: [{ url: `${SITE_URL}/og.png`, width: 1200, height: 630 }],
49 + title: "UQO Éval — Laboratoire pédagogique d'évaluation immobilière",
55 50 description:
56 − "Estimation immobilière transparente pour le Québec : 3,7 millions de propriétés estimées, calcul montré en entier.",
51 + "Comprendre comment une propriété est évaluée : 3,7 millions de propriétés du Québec, le calcul montré en entier.",
57 52 },
58 53 formatDetection: { telephone: false },
59 54 };
@@ -70,23 +65,27 @@ const ORG_JSONLD = {
70 65 "@context": "https://schema.org",
71 66 "@graph": [
72 67 {
73 − "@type": "Organization",
68 + "@type": "EducationalOrganization",
74 69 "@id": `${SITE_URL}/#org`,
75 − name: "Vrai-Prix",
70 + name: "UQO Éval",
71 + alternateName: "Laboratoire pédagogique d'évaluation immobilière",
76 72 url: SITE_URL,
77 − email: "contact@spboucher.ai",
78 − founder: { "@type": "Person", name: "Simon-Pierre Boucher" },
73 + parentOrganization: {
74 + "@type": "CollegeOrUniversity",
75 + name: "Université du Québec en Outaouais",
76 + url: "https://uqo.ca",
77 + },
79 78 areaServed: { "@type": "AdministrativeArea", name: "Québec, Canada" },
80 79 },
81 80 {
82 81 "@type": "WebSite",
83 82 "@id": `${SITE_URL}/#site`,
84 − name: "Vrai-Prix",
83 + name: "UQO Éval",
85 84 url: SITE_URL,
86 85 inLanguage: "fr-CA",
87 86 publisher: { "@id": `${SITE_URL}/#org` },
88 87 description:
89 − "Estimation immobilière transparente pour le Québec : modèle hédonique + comparables réels, fourchette honnête et indice de confiance.",
88 + "Laboratoire pédagogique d'évaluation immobilière : modèle hédonique, comparables réels, intervalle de confiance — le calcul montré en entier.",
90 89 },
91 90 ],
92 91 };
@@ -95,11 +94,12 @@ const TICKER = [
95 94 "3 747 008 unités d'évaluation",
96 95 "745 119 ventes réelles 2021-2026",
97 96 "22 M d'estimations calculées",
98 − "Fourchette honnête P10-P90",
97 + "Intervalle de confiance P10-P90",
99 98 "Indice de confiance A-D",
100 − "Zéro boîte noire — le calcul est montré",
99 + "Chaque ajustement montré en dollars",
101 100 "1 100 municipalités couvertes",
102 − "Erreur médiane 11 %",
101 + "Modèle hédonique — erreur médiane 11 %",
102 + "Cours IMM1003 · IMM1033 — UQO",
103 103 ];
104 104
105 105 export default function RootLayout({ children }: LayoutProps<"/">) {
@@ -113,23 +113,29 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
113 113 dangerouslySetInnerHTML={{ __html: JSON.stringify(ORG_JSONLD) }}
114 114 />
115 115 <LangProvider>
116 − {/* header opaque compact (standard header/menu KA 2026-08-22) */}
116 + {/* header opaque compact — logo officiel UQO + wordmark Éval */}
117 117 <header className="sticky top-0 z-[var(--z-header,500)] border-b border-[var(--line-strong)] bg-paper">
118 118 <div className="relative mx-auto flex h-14 max-w-6xl items-center gap-6 px-4 sm:px-6">
119 119 <Link
120 120 href="/"
121 − className="vp-display relative flex items-baseline gap-1.5 text-[19px] font-bold leading-none tracking-[-0.04em] after:absolute after:-inset-2 after:content-['']"
121 + className="relative flex items-end gap-1.5 after:absolute after:-inset-2 after:content-['']"
122 + aria-label="UQO Éval — accueil"
122 123 >
123 − Vrai&nbsp;Prix
124 − <span
125 − aria-hidden="true"
126 − className="inline-block h-[7px] w-[7px] bg-accent"
124 + {/* eslint-disable-next-line @next/next/no-img-element */}
125 + <img
126 + src="/uqo/uqo-bleu.png"
127 + alt="UQO"
128 + className="h-[26px] w-auto"
127 129 />
130 + <span className="vp-display text-[19px] font-bold leading-none tracking-[-0.04em] text-accent">
131 + Éval
132 + </span>
128 133 </Link>
129 − {/* badge Groupe KA — desktop seulement (l'espace manque sous xl ;
130 − le menu mobile porte sa propre entrée) */}
131 − <span className="hidden xl:inline-flex">
132 − <GroupeKaBadge />
134 + {/* sous-titre — desktop large seulement */}
135 + <span className="vp-mono hidden text-[9.5px] uppercase leading-tight tracking-[0.1em] text-ink-3 xl:inline-block">
136 + Laboratoire pédagogique
137 + <br />
138 + d&apos;évaluation immobilière
133 139 </span>
134 140 <div className="ml-auto flex items-center gap-3 sm:gap-4">
135 141 <HeaderNav />
@@ -137,7 +143,6 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
137 143 aria-hidden="true"
138 144 className="hidden h-4 w-px bg-[var(--line)] lg:inline-block"
139 145 />
140 − <AccountButton />
141 146 <LangToggle />
142 147 </div>
143 148 </div>
@@ -151,41 +156,35 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
151 156 </div>
152 157 <main className="mx-auto max-w-6xl px-4 pb-16 sm:px-6">{children}</main>
153 158 {/* rangée locale (papier) : marque + liens de navigation du site,
154 − puis le bloc encre final = KaFooter (commun Groupe KA) */}
159 + puis le bloc encre final = footer institutionnel UQO Éval */}
155 160 <div className="mx-auto max-w-6xl px-4 sm:px-6">
156 161 <div className="border-t-2 border-ink pt-7 text-[13px] text-ink-2">
157 162 <p className="vp-display mb-3 flex items-baseline gap-2 text-[24px] font-bold tracking-[-0.04em] text-ink">
158 − Vrai&nbsp;Prix
163 + UQO&nbsp;Éval
159 164 <span
160 165 aria-hidden="true"
161 166 className="inline-block h-[8px] w-[8px] bg-accent"
162 167 />
163 168 </p>
164 169 <p className="max-w-2xl">
165 − Estimations bâties sur les rôles d&apos;évaluation foncière du
166 − Québec (MAMH, données ouvertes) et 745 119 transactions réelles.
167 − Modèle hédonique + comparables ajustés, calcul montré en entier.
168 − Estimation statistique à titre indicatif.
170 + Laboratoire pédagogique d&apos;évaluation immobilière. Estimations
171 + bâties sur les rôles d&apos;évaluation foncière du Québec (MAMH,
172 + données ouvertes) et 745 119 transactions réelles. Modèle
173 + hédonique + comparables ajustés, calcul montré en entier — à des
174 + fins pédagogiques, universitaires et expérimentales.
169 175 </p>
170 176 <FooterNav />
171 177 </div>
172 178 </div>
173 − <KaFooter
174 − siteId="vrai-prix"
175 − localLegal={[
176 − { label: "Conditions", href: "/conditions" },
177 − { label: "Confidentialité", href: "/confidentialite" },
178 − { label: "Méthodologie", href: "/methodologie" },
179 − ]}
180 − />
179 + <UqoFooter />
181 180 <MobileTabBar />
182 181 <CookieConsent />
183 182 <script
184 183 dangerouslySetInnerHTML={{
185 − __html: 'window.KA_AGENT={site:"vrai-prix"}',
184 + __html: 'window.KA_AGENT={site:"uqo-eval"}',
186 185 }}
187 186 />
188 − <script src="/ka-agent.js" defer />
187 + <script src="/assistant.js" defer />
189 188 </LangProvider>
190 189 </body>
191 190 </html>
modified src/app/methodologie/MethodologieClient.tsx +48 −6
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — méthodologie complète + guide pédagogique des méthodes d'évaluation.
2 2 "use client";
3 3 import Link from "next/link";
4 4 import { useLang } from "@/components/LangContext";
@@ -17,7 +17,7 @@ const FR: Section[] = [
17 17 num: "01",
18 18 title: "Données sources",
19 19 paras: [
20 − "Deux corpus publics forment la fondation de Vrai-Prix. Premièrement, les rôles d'évaluation foncière géoréférencés du Québec, publiés en données ouvertes par le ministère des Affaires municipales et de l'Habitation (MAMH) : six millésimes complets (2021 à 2026), de 3 626 120 à 3 747 008 unités d'évaluation par millésime, soit 22,15 millions d'observations unité-année. Chaque unité porte sa géométrie ponctuelle (NAD83, EPSG:4269), son matricule, son usage (code CUBF), ses caractéristiques de bâtiment et de terrain, et ses valeurs au rôle.",
20 + "Deux corpus publics forment la fondation d'UQO Éval. Premièrement, les rôles d'évaluation foncière géoréférencés du Québec, publiés en données ouvertes par le ministère des Affaires municipales et de l'Habitation (MAMH) : six millésimes complets (2021 à 2026), de 3 626 120 à 3 747 008 unités d'évaluation par millésime, soit 22,15 millions d'observations unité-année. Chaque unité porte sa géométrie ponctuelle (NAD83, EPSG:4269), son matricule, son usage (code CUBF), ses caractéristiques de bâtiment et de terrain, et ses valeurs au rôle.",
21 21 "Deuxièmement, 745 119 transactions immobilières publiées entre janvier 2021 et juillet 2026, couvrant 1 100 municipalités, chacune géolocalisée et documentée (prix, date, type, année de construction, aire d'étages, valeurs au rôle).",
22 22 ],
23 23 kv: [
@@ -93,6 +93,27 @@ const FR: Section[] = [
93 93 "L'honnêteté méthodologique exige de nommer ce que le système ne voit pas. Les rénovations intérieures, garages, piscines et l'état d'entretien n'apparaissent pas dans les données ouvertes du rôle. Dans les marchés ruraux minces, la dispersion dépasse les cibles IAAO urbaines — l'indice de confiance le reflète. Les unités d'une même tour à condos partagent un point géographique : sans numéro d'appartement, la recherche peut retourner une unité voisine. Les valeurs au rôle utilisées comme variables sont établies 18 à 24 mois avant leur entrée en vigueur. Enfin, une estimation statistique, aussi rigoureuse soit-elle, ne remplace pas l'évaluation d'un évaluateur agréé (OEAQ) ni l'analyse d'un courtier pour une mise en vente.",
94 94 ],
95 95 },
96 + {
97 + num: "08",
98 + title: "Guide pédagogique — les méthodes d'évaluation",
99 + paras: [
100 + "Méthode des comparables (comparaison directe) : la valeur est déduite de ventes récentes de propriétés semblables, chacune ajustée pour ses différences (marché, superficie, âge…). C'est la méthode privilégiée pour le résidentiel — et la composante « comparables » du moteur (section 05) en est une automatisation : sélection par rayon et similarité, ajustements en dollars, médiane pondérée.",
101 + "Méthode hédonique (régression) : le prix est modélisé comme une fonction des caractéristiques de la propriété — chaque attribut (superficie, localisation, âge…) porte un « prix implicite ». Le modèle LightGBM de la section 03 est une forme moderne et non linéaire de régression hédonique ; la régression quantile (P10/P90) en dérive l'intervalle de confiance.",
102 + "Analyse du prix au pied carré / au mètre carré : diviser le prix par la superficie donne un indicateur simple et comparable entre propriétés — mais il ignore le terrain, l'âge et la qualité. La plateforme l'affiche partout comme repère, jamais comme conclusion.",
103 + "Ajustement temporel : une vente d'il y a deux ans ne vaut pas une vente d'hier. L'indice de marché mensuel (section 06) ramène chaque prix aux conditions actuelles avant toute comparaison — c'est l'équivalent quantitatif de l'« ajustement de conditions de marché » du rapport d'évaluation classique.",
104 + "AVM (Automated Valuation Model) : un système qui produit des estimations de masse sans intervention humaine — c'est exactement ce qu'est UQO Éval. Les AVM sont encadrés par la norme IAAO Standard on AVMs : validation par études de ratios (section 04), publication des limites, indice de confiance obligatoire.",
105 + "Méthode du coût (IMM1033) : la valeur = coût de remplacement du bâtiment, moins la dépréciation, plus la valeur du terrain. Le rôle d'évaluation en publie les ingrédients (valeur terrain vs bâtiment, section « composition de la valeur » de chaque fiche) — la plateforme les affiche pour permettre cette lecture, même si son moteur repose sur les deux premières méthodes.",
106 + ],
107 + },
108 + {
109 + num: "09",
110 + title: "Lire une estimation de façon critique — intervalle, biais et limites",
111 + paras: [
112 + "L'intervalle de confiance (P10-P90) dit : « 80 % des propriétés semblables se vendent dans cette fourchette ». Un intervalle étroit signale un marché liquide et homogène ; un intervalle large, un marché mince ou une propriété atypique. Une estimation ponctuelle sans intervalle est une illusion de précision.",
113 + "Biais connus des modèles automatisés : sous-estimation des propriétés haut de gamme et surestimation de l'entrée de gamme (compression vers la moyenne — visible dans le PRB négatif de la section 04) ; retard sur les retournements de marché (le modèle apprend du passé) ; cécité à l'état intérieur, aux rénovations et aux nuisances locales non cartographiées.",
114 + "Réflexe d'évaluateur : confronter l'estimation aux comparables affichés (sont-ils vraiment semblables ? proches ? récents ?), à la valeur au rôle (l'écart est-il explicable ?) et au prix demandé le cas échéant (stratégie de mise en marché ≠ valeur marchande). L'indice de confiance A-D résume cette qualité de preuve — un « D » commande la prudence, pas la conclusion.",
115 + ],
116 + },
96 117 ];
97 118
98 119 const EN: Section[] = [
@@ -100,7 +121,7 @@ const EN: Section[] = [
100 121 num: "01",
101 122 title: "Source data",
102 123 paras: [
103 − "Two public corpora form the foundation of Vrai-Prix. First, Québec's georeferenced property assessment rolls, published as open data by the MAMH: six complete vintages (2021-2026), 3.63M to 3.75M assessment units each — 22.15 million unit-year observations, each with point geometry (NAD83), roll number, CUBF use code, building and lot characteristics, and assessed values.",
124 + "Two public corpora form the foundation of UQO Éval. First, Québec's georeferenced property assessment rolls, published as open data by the MAMH: six complete vintages (2021-2026), 3.63M to 3.75M assessment units each — 22.15 million unit-year observations, each with point geometry (NAD83), roll number, CUBF use code, building and lot characteristics, and assessed values.",
104 125 "Second, 745,119 real-estate transactions published between January 2021 and July 2026 across 1,100 municipalities, each geolocated and documented (price, date, type, year built, floor area, assessed values).",
105 126 ],
106 127 kv: [
@@ -176,6 +197,27 @@ const EN: Section[] = [
176 197 "Methodological honesty requires naming what the system cannot see. Interior renovations, garages, pools and upkeep do not appear in open roll data. In thin rural markets, dispersion exceeds urban IAAO targets — the confidence index reflects it. Units in the same condo tower share one geographic point: without an apartment number, search may return a neighbouring unit. Assessed values used as features are set 18-24 months before taking effect. Finally, a statistical estimate, however rigorous, does not replace a chartered appraiser's evaluation (OEAQ) or a broker's analysis for a listing.",
177 198 ],
178 199 },
200 + {
201 + num: "08",
202 + title: "Teaching guide — the appraisal methods",
203 + paras: [
204 + "Sales-comparison (direct comparison) approach: value is inferred from recent sales of similar properties, each adjusted for its differences (market, area, age…). It is the preferred method for residential — and the engine's comparables component (section 05) is an automation of it: radius-and-similarity selection, dollar adjustments, weighted median.",
205 + "Hedonic method (regression): price is modelled as a function of the property's characteristics — each attribute (area, location, age…) carries an 'implicit price'. The LightGBM model of section 03 is a modern, non-linear form of hedonic regression; quantile regression (P10/P90) derives its confidence interval.",
206 + "Price-per-square-foot / per-square-metre analysis: dividing price by area yields a simple, comparable indicator — but it ignores land, age and quality. The platform displays it everywhere as a reference point, never as a conclusion.",
207 + "Time adjustment: a two-year-old sale is not yesterday's sale. The monthly market index (section 06) brings every price to current conditions before any comparison — the quantitative equivalent of the classic appraisal 'market-conditions adjustment'.",
208 + "AVM (Automated Valuation Model): a system producing mass estimates without human intervention — exactly what UQO Éval is. AVMs are governed by the IAAO Standard on AVMs: ratio-study validation (section 04), published limitations, mandatory confidence index.",
209 + "Cost approach (IMM1033): value = replacement cost of the building, minus depreciation, plus land value. The assessment roll publishes its ingredients (land vs building value — the 'value composition' block of every property page) — the platform displays them to enable this reading, even though its engine relies on the first two methods.",
210 + ],
211 + },
212 + {
213 + num: "09",
214 + title: "Reading an estimate critically — interval, bias and limits",
215 + paras: [
216 + "The confidence interval (P10-P90) says: '80% of similar properties sell within this range.' A narrow interval signals a liquid, homogeneous market; a wide one, a thin market or an atypical property. A point estimate without an interval is an illusion of precision.",
217 + "Known biases of automated models: underestimating high-end properties and overestimating entry-level ones (compression toward the mean — visible in section 04's negative PRB); lagging market turning points (the model learns from the past); blindness to interior condition, renovations and unmapped local nuisances.",
218 + "An appraiser's reflex: confront the estimate with the displayed comparables (are they truly similar? close? recent?), with the assessed value (is the gap explainable?) and with the asking price when available (marketing strategy ≠ market value). The A-D confidence index summarizes this quality of evidence — a 'D' commands caution, not conclusion.",
219 + ],
220 + },
179 221 ];
180 222
181 223 export default function MethodologieClient() {
@@ -186,7 +228,7 @@ export default function MethodologieClient() {
186 228 <div className="py-10">
187 229 <nav className="vp-mono mb-8 flex items-center gap-2 text-[11.5px] uppercase tracking-[0.06em] text-ink-3">
188 230 <Link href="/" className="border-b-[1.5px] border-transparent hover:border-green hover:text-green">
189 − Vrai-Prix
231 + UQO Éval
190 232 </Link>
191 233 <span>/</span>
192 234 <span>{fr ? "Méthodologie" : "Methodology"}</span>
@@ -209,8 +251,8 @@ export default function MethodologieClient() {
209 251 </h1>
210 252 <p className="mt-4 max-w-2xl text-[15.5px] leading-relaxed text-ink-2">
211 253 {fr
212 − ? "Notre différenciateur est la transparence totale. Cette page documente chaque étape — données, fusion, modèle, validation, moteur — au niveau de détail d'un rapport technique."
213 − : "Our differentiator is total transparency. This page documents every step — data, merge, model, validation, engine — at technical-report depth."}
254 + ? "La transparence totale est la raison d'être de ce laboratoire pédagogique. Cette page documente chaque étape — données, fusion, modèle, validation, moteur — au niveau de détail d'un rapport technique, puis situe la plateforme dans le paysage des méthodes d'évaluation (cours IMM1003 · IMM1033, UQO)."
255 + : "Total transparency is this teaching laboratory's raison d'être. This page documents every step — data, merge, model, validation, engine — at technical-report depth, then situates the platform within the landscape of appraisal methods (courses IMM1003 · IMM1033, UQO)."}
214 256 </p>
215 257
216 258 {/* la chaîne de calcul, d'un coup d'œil */}
modified src/app/methodologie/page.tsx +2 −2
@@ -1,11 +1,11 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata } from "next";
3 3 import MethodologieClient from "./MethodologieClient";
4 4
5 5 export const metadata: Metadata = {
6 6 title: "Méthodologie — modèle hédonique et comparables expliqués",
7 7 description:
8 − "Comment Vrai-Prix estime la valeur des propriétés du Québec : rôles d'évaluation MAMH (22 M d'observations), 745 119 ventes réelles, modèle hédonique LightGBM, comparables ajustés, erreur médiane 11 %, normes IAAO.",
8 + "Comment UQO Éval estime la valeur des propriétés du Québec : rôles d'évaluation MAMH (22 M d'observations), 745 119 ventes réelles, modèle hédonique LightGBM, comparables ajustés, erreur médiane 11 %, normes IAAO.",
9 9 alternates: { canonical: "/methodologie" },
10 10 };
11 11
modified src/app/municipalite/[slug]/[type]/page.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Page programmatique municipalité × type de propriété.
3 3 import type { Metadata } from "next";
4 4 import Link from "next/link";
modified src/app/municipalite/[slug]/page.tsx +2 −2
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Page programmatique par municipalité : stats, répartition par type,
3 3 // fiches en HTML (maillage vers /estimation/[id]) et municipalités voisines.
4 4 import type { Metadata } from "next";
@@ -101,7 +101,7 @@ export default async function MunicipalitePage({
101 101 </h1>
102 102 <p className="mt-4 max-w-2xl text-[15.5px] text-ink-2">
103 103 {numFr(s.n)} unités d&apos;évaluation au rôle foncier {EST_YEAR},
104 − estimées par le modèle Vrai-Prix (modèle hédonique + ventes réelles).
104 + estimées par le modèle UQO Éval (modèle hédonique + ventes réelles).
105 105 Cherchez n&apos;importe quelle adresse de {mun.nom} depuis{" "}
106 106 <Link href="/" className="underline">
107 107 l&apos;accueil
modified src/app/municipalites/page.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Index des 1 097 municipalités : point d'entrée du maillage programmatique.
3 3 import type { Metadata } from "next";
4 4 import Link from "next/link";
modified src/app/page.tsx +8 −11
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — accueil (données serveur : indice, ventes récentes, municipalités).
2 2 import type { Metadata } from "next";
3 3 import Link from "next/link";
4 4 import HomeClient, { type PulseItem, type RecentSaleItem } from "./HomeClient";
@@ -12,23 +12,20 @@ export const revalidate = 3600;
12 12
13 13 export const metadata: Metadata = {
14 14 title: {
15 − absolute: "Vrai-Prix — estimation immobilière gratuite au Québec · Un service Groupe KA",
15 + absolute: "UQO Éval — Laboratoire pédagogique d'évaluation immobilière",
16 16 },
17 17 description:
18 − "Estimez gratuitement la valeur de n'importe quelle propriété du Québec : 3,7 millions d'unités d'évaluation, 745 119 ventes réelles, fourchette honnête P10-P90 et calcul montré en entier.",
18 + "Comprendre comment une propriété est évaluée au Québec : 3,7 millions d'unités d'évaluation, 745 119 ventes réelles, modèle hédonique, comparables ajustés, intervalle de confiance P10-P90 — le calcul montré en entier.",
19 19 alternates: { canonical: "/" },
20 20 openGraph: {
21 21 type: "website",
22 − siteName: "Vrai-Prix",
22 + siteName: "UQO Éval",
23 23 locale: "fr_CA",
24 24 url: "/",
25 − title:
26 − "Vrai-Prix — estimation immobilière gratuite au Québec · Un service Groupe KA",
25 + title: "UQO Éval — Laboratoire pédagogique d'évaluation immobilière",
27 26 description:
28 − "Estimez gratuitement la valeur de n'importe quelle propriété du Québec : 3,7 millions d'unités d'évaluation, 745 119 ventes réelles, fourchette honnête P10-P90 et calcul montré en entier.",
29 − images: [
30 − { url: "https://www.vrai-prix.com/og.png", width: 1200, height: 630 },
31 − ],
27 + "Comprendre comment une propriété est évaluée au Québec : 3,7 millions d'unités d'évaluation, 745 119 ventes réelles, intervalle de confiance P10-P90 — le calcul montré en entier.",
28 + images: [{ url: "/og.png", width: 1200, height: 630 }],
32 29 },
33 30 };
34 31
@@ -90,7 +87,7 @@ export default function Home() {
90 87 {/* ---- Explorer par municipalité (maillage interne indexable) ---- */}
91 88 <section className="mt-14">
92 89 <header className="sec">
93 − <span className="sec-num">05 — Explorer par municipalité</span>
90 + <span className="sec-num">06 — Explorer par municipalité</span>
94 91 <h2 className="vp-display mt-2 max-w-3xl text-[clamp(24px,3.6vw,36px)] font-bold uppercase leading-[1.02] tracking-[-0.03em]">
95 92 La valeur des propriétés, ville par ville
96 93 </h2>
modified src/app/parc/ParcClient.tsx +3 −14
@@ -1,11 +1,10 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import { useEffect, useState } from "react";
4 4 import Link from "next/link";
5 5 import SearchBox, { type Suggestion } from "@/components/SearchBox";
6 6 import { CountUp, locNum, Sparkline, ValueSplit } from "@/components/MetricViz";
7 7 import { useLang } from "@/components/LangContext";
8 −import FavButton from "@/components/FavButton";
9 8 import type { PortfolioResult } from "@/lib/estimator";
10 9 import type { TKey } from "@/lib/i18n";
11 10
@@ -108,8 +107,8 @@ export default function ParcClient() {
108 107 </h1>
109 108 <p className="mt-4 max-w-xl text-[16px] text-ink-2">
110 109 {fr
111 − ? "Ajoutez chacune de vos adresses. Vrai-Prix évalue chaque propriété, agrège votre parc — valeur totale, fourchette, croissance 2021→2026 — et produit un rapport PDF consolidé."
112 − : "Add each of your addresses. Vrai-Prix values every property, aggregates your portfolio — total value, range, 2021→2026 growth — and produces a consolidated PDF report."}
110 + ? "Ajoutez chacune de vos adresses. UQO Éval évalue chaque propriété, agrège votre parc — valeur totale, fourchette, croissance 2021→2026 — et produit un rapport PDF consolidé."
111 + : "Add each of your addresses. UQO Éval values every property, aggregates your portfolio — total value, range, 2021→2026 growth — and produces a consolidated PDF report."}
113 112 </p>
114 113 </section>
115 114
@@ -319,16 +318,6 @@ export default function ParcClient() {
319 318 </p>
320 319 </div>
321 320 <span className="flex shrink-0 items-center gap-2">
322 − {/* ♥ Mon univers Ka — favori unifié Groupe KA */}
323 − <FavButton
324 − item={{
325 − item_id: u.id,
326 − title: u.adresse ?? u.id,
327 − subtitle: u.municipalite ?? "",
328 − price_label: fmt(r.estimate, lang),
329 − url: `https://www.vrai-prix.com/estimation/${encodeURIComponent(u.id)}`,
330 − }}
331 − />
332 321 <span className={`pill ${CONF[r.confidenceLevel]}`}>
333 322 {r.confidenceLevel}
334 323 </span>
modified src/app/parc/page.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { Metadata } from "next";
3 3 import ParcClient from "./ParcClient";
4 4
modified src/app/robots.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import type { MetadataRoute } from "next";
3 3 import { SITE_URL } from "@/lib/seo";
4 4
modified src/app/sitemap-index.xml/route.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Index de sitemaps (protocole sitemaps.org) : le sitemap des pages
3 3 // (/sitemap.xml) + les ~84 sitemaps des fiches (/estimation/sitemap/N.xml).
4 4 // C'est l'URL à soumettre dans Google Search Console.
modified src/app/sitemap.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Sitemap des pages : statiques + 1 097 municipalités + pages municipalité×type.
3 3 // Les 3,7 M de fiches sont dans /estimation/sitemap/[id].xml (voir sitemap-index.xml).
4 4 import type { MetadataRoute } from "next";
modified src/app/stats/page.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import StatsView, { type ProvStats } from "@/components/StatsView";
3 3 import StatsDashboard, { type DashboardPayload } from "@/components/StatsDashboard";
4 4 import { getDashboard } from "@/lib/stats-dashboard";
deleted src/components/AccountButton.tsx +0 −81
@@ -1,81 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −/**
3 − * Bouton compte de l'en-tête (KA ID — SSO du Groupe KA).
4 − * · Déconnecté : pilule « KA ID » → /api/auth/ka/login?next=<page courante>
5 − * · Connecté : avatar (ou initiale) + prénom → /compte
6 − * Composant client : interroge /api/auth/me au montage (session cookie httpOnly).
7 − */
8 −"use client";
9 −import { useEffect, useState } from "react";
10 −import Link from "next/link";
11 −import { usePathname } from "next/navigation";
12 −import { useLang } from "./LangContext";
13 −
14 −type MeUser = {
15 − ka_id: string;
16 − email: string;
17 − name: string;
18 − picture: string;
19 − provider: string;
20 −};
21 −
22 −export default function AccountButton() {
23 − const { lang } = useLang();
24 − const fr = lang === "fr";
25 − const path = usePathname();
26 − // undefined = chargement (on ne montre rien pour éviter le clignotement)
27 − const [user, setUser] = useState<MeUser | null | undefined>(undefined);
28 −
29 − useEffect(() => {
30 − let alive = true;
31 − fetch("/api/auth/me")
32 − .then((r) => r.json())
33 − .then((d) => alive && setUser(d?.user ?? null))
34 − .catch(() => alive && setUser(null));
35 − return () => {
36 − alive = false;
37 − };
38 − }, [path]);
39 −
40 − if (user === undefined) {
41 − return <span className="min-h-[36px] w-[52px]" aria-hidden="true" />;
42 − }
43 −
44 − if (user === null) {
45 − return (
46 − <a
47 − href={`/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`}
48 − className="vp-mono inline-flex min-h-[36px] items-center rounded-[2px] bg-ink px-3.5 py-1.5 text-[11px] font-medium uppercase tracking-[0.1em] text-paper transition-colors hover:bg-accent"
49 − title={fr ? "Se connecter avec KA ID" : "Sign in with KA ID"}
50 − >
51 − KA&nbsp;ID
52 − </a>
53 − );
54 − }
55 −
56 − const prenom = (user.name || user.email).split(" ")[0];
57 − return (
58 − <Link
59 − href="/compte"
60 − className="group inline-flex min-h-[36px] items-center gap-2 py-1"
61 − title={fr ? "Mon compte — carte de membre Groupe KA" : "My account — Groupe KA member card"}
62 − >
63 − {user.picture ? (
64 − // eslint-disable-next-line @next/next/no-img-element
65 − <img
66 − src={user.picture}
67 − alt=""
68 − referrerPolicy="no-referrer"
69 − className="h-7 w-7 rounded-full object-cover grayscale transition-[filter] group-hover:grayscale-0"
70 − />
71 − ) : (
72 − <span className="vp-display flex h-7 w-7 items-center justify-center rounded-full bg-ink text-[13px] font-bold text-paper">
73 − {prenom.charAt(0).toUpperCase()}
74 − </span>
75 − )}
76 − <span className="vp-display max-w-[92px] truncate border-b-2 border-transparent text-[13px] font-bold text-ink group-hover:border-accent">
77 − {prenom}
78 − </span>
79 − </Link>
80 − );
81 −}
modified src/components/CookieConsent.tsx +3 −3
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * Gestionnaire de consentement aux témoins (Loi 25) :
4 4 * bandeau au premier passage + panneau de préférences réouvrable partout
@@ -39,7 +39,7 @@ export function openCookiePrefs() {
39 39 const TXT = {
40 40 fr: {
41 41 banner:
42 − "Vrai-Prix utilise un minimum de témoins : des essentiels (fonctionnement du site) et, si vous l'acceptez, un témoin de préférences pour retenir votre langue. Aucun témoin publicitaire, aucun pistage tiers.",
42 + "UQO Éval utilise un minimum de témoins : des essentiels (fonctionnement du site) et, si vous l'acceptez, un témoin de préférences pour retenir votre langue. Aucun témoin publicitaire, aucun pistage tiers.",
43 43 acceptAll: "Tout accepter",
44 44 essentialOnly: "Essentiels seulement",
45 45 customize: "Personnaliser",
@@ -56,7 +56,7 @@ const TXT = {
56 56 },
57 57 en: {
58 58 banner:
59 − "Vrai-Prix uses a minimum of cookies: essential ones (site operation) and, with your consent, a preference cookie to remember your language. No advertising cookies, no third-party tracking.",
59 + "UQO Éval uses a minimum of cookies: essential ones (site operation) and, with your consent, a preference cookie to remember your language. No advertising cookies, no third-party tracking.",
60 60 acceptAll: "Accept all",
61 61 essentialOnly: "Essential only",
62 62 customize: "Customize",
modified src/components/EstimateForm.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import { useState } from "react";
4 4 import { useRouter } from "next/navigation";
deleted src/components/FavButton.tsx +0 −148
@@ -1,148 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −/**
3 − * Bouton ♥ « Mon univers Ka » — favoris unifiés du Groupe KA (KA ID).
4 − * · Connecté : bascule le favori au hub via POST /api/favorites/toggle
5 − * (synchrone — le hub groupe-ka.com est le magasin central, rien en local).
6 − * · Déconnecté : le clic envoie au SSO KA ID (retour sur la page courante).
7 − * L'état initial est lu via GET /api/favorites, partagé entre tous les boutons
8 − * de la page (une seule requête, mini-cache module 15 s, invalidé au toggle).
9 − * Utilisable dans un <Link> parent (stopPropagation + preventDefault).
10 − */
11 −"use client";
12 −import { useEffect, useState } from "react";
13 −import { usePathname } from "next/navigation";
14 −import { useLang } from "./LangContext";
15 −
16 −export type FavPayload = {
17 − item_id: string;
18 − title: string;
19 − subtitle?: string;
20 − price_label?: string;
21 − image_url?: string;
22 − url?: string;
23 −};
24 −
25 −type FavState = { authed: boolean; ids: string[] };
26 −
27 −// -- mini-cache module : une requête partagée par page, TTL 15 s --------------
28 −const TTL_MS = 15_000;
29 −let shared: { at: number; p: Promise<FavState> } | null = null;
30 −
31 −function loadFavs(): Promise<FavState> {
32 − const now = Date.now();
33 − if (shared && now - shared.at < TTL_MS) return shared.p;
34 − const p: Promise<FavState> = fetch("/api/favorites")
35 − .then(async (r) => {
36 − if (r.status === 401) return { authed: false, ids: [] };
37 − if (!r.ok) throw new Error(String(r.status));
38 − const d = (await r.json()) as { favorites?: { item_id?: string }[] };
39 − return {
40 − authed: true,
41 − ids: (d.favorites ?? []).map((f) => String(f.item_id ?? "")),
42 − };
43 − })
44 − .catch(() => {
45 − shared = null; // erreur transitoire : jamais mise en cache
46 − return { authed: true, ids: [] };
47 − });
48 − shared = { at: now, p };
49 − return p;
50 −}
51 −
52 −export default function FavButton({
53 − item,
54 − size = "sm",
55 − className = "",
56 −}: {
57 − item: FavPayload;
58 − size?: "sm" | "lg";
59 − className?: string;
60 −}) {
61 − const { lang } = useLang();
62 − const fr = lang === "fr";
63 − const path = usePathname();
64 − const [authed, setAuthed] = useState<boolean | null>(null);
65 − const [fav, setFav] = useState(false);
66 − const [busy, setBusy] = useState(false);
67 − const [flash, setFlash] = useState(false); // échec hub — bref signal visuel
68 −
69 − useEffect(() => {
70 − let alive = true;
71 − loadFavs().then((st) => {
72 − if (!alive) return;
73 − setAuthed(st.authed);
74 − setFav(st.ids.includes(item.item_id));
75 − });
76 − return () => {
77 − alive = false;
78 − };
79 − }, [item.item_id]);
80 −
81 − const onClick = async (e: React.MouseEvent) => {
82 − // le bouton vit parfois dans un <Link> (carte de résultat) : ne pas naviguer
83 − e.preventDefault();
84 − e.stopPropagation();
85 − if (busy) return;
86 − if (authed === false) {
87 − window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`;
88 − return;
89 − }
90 − setBusy(true);
91 − try {
92 − const r = await fetch("/api/favorites/toggle", {
93 − method: "POST",
94 − headers: { "Content-Type": "application/json" },
95 − body: JSON.stringify({ ...item, action: fav ? "remove" : "add" }),
96 − });
97 − if (r.status === 401) {
98 − window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`;
99 − return;
100 − }
101 − if (!r.ok) throw new Error(String(r.status));
102 − const d = (await r.json()) as { favorited?: boolean };
103 − setFav(!!d.favorited);
104 − shared = null; // les autres boutons reliront une liste fraîche
105 − } catch {
106 − setFlash(true);
107 − setTimeout(() => setFlash(false), 1200);
108 − } finally {
109 − setBusy(false);
110 − }
111 − };
112 −
113 − const label = fav
114 − ? fr
115 − ? "Retirer de mes favoris"
116 − : "Remove from my favourites"
117 − : fr
118 − ? "Ajouter à mes favoris — Mon univers Ka"
119 − : "Add to my favourites — My Ka universe";
120 − const dims =
121 − size === "lg" ? "h-11 w-11 text-[19px]" : "h-8 w-8 text-[14px]";
122 −
123 − return (
124 − <button
125 − type="button"
126 − onClick={onClick}
127 − disabled={busy}
128 − aria-pressed={fav}
129 − aria-label={label}
130 − title={
131 − flash
132 − ? fr
133 − ? "Hub Groupe KA injoignable — réessayez"
134 − : "Groupe KA hub unreachable — try again"
135 − : label
136 − }
137 − className={`flex shrink-0 items-center justify-center rounded-full border-[1.5px] border-ink leading-none transition-all ${dims} ${
138 − fav
139 − ? "bg-ink text-lime hover:bg-green-deep"
140 − : "bg-surface text-ink-3 hover:bg-lime-soft hover:text-ink"
141 − } ${busy ? "opacity-60" : ""} ${flash ? "!border-red-600 !text-red-600" : ""} ${className}`}
142 − >
143 − <span aria-hidden="true" className="translate-y-[-1px]">
144 − {fav ? "♥" : "♡"}
145 − </span>
146 − </button>
147 − );
148 −}
modified src/components/FooterNav.tsx +3 −6
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — liens de pied de page (rangée papier)
2 2 "use client";
3 3 import Link from "next/link";
4 4 import { useLang } from "./LangContext";
@@ -12,8 +12,8 @@ export default function FooterNav() {
12 12 "vp-mono text-[11px] uppercase tracking-[0.08em] text-ink-2 border-b border-transparent hover:text-green-deep hover:border-green-deep transition-colors";
13 13 return (
14 14 <nav className="mt-5 flex flex-wrap items-center gap-x-6 gap-y-2">
15 − <Link href="/ka" className={cls}>
16 − {fr ? "Ka · agent IA" : "Ka · AI agent"}
15 + <Link href="/assistant" className={cls}>
16 + {fr ? "Assistant IA" : "AI assistant"}
17 17 </Link>
18 18 <Link href="/parc" className={cls}>
19 19 {fr ? "Parc immobilier" : "Portfolio"}
@@ -36,9 +36,6 @@ export default function FooterNav() {
36 36 <Link href="/contact" className={cls}>
37 37 Contact
38 38 </Link>
39 − <a href="/doc/index.html" className={cls}>
40 − Documentation
41 − </a>
42 39 <button onClick={openCookiePrefs} className={`${cls} cursor-pointer`}>
43 40 🍪 {fr ? "Préférences de témoins" : "Cookie preferences"}
44 41 </button>
modified src/components/HeaderNav.tsx +15 −14
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * Navigation d'en-tête. Le menu mobile utilise le motif checkbox + peer-checked :
4 4 * il s'ouvre et se ferme en PUR CSS — fonctionne même sans JavaScript (cache
@@ -10,7 +10,6 @@ import { useEffect, useRef } from "react";
10 10 import Link from "next/link";
11 11 import { usePathname } from "next/navigation";
12 12 import { useLang } from "./LangContext";
13 −import GroupeKaBadge from "../ka/GroupeKaBadge";
14 13
15 14 export default function HeaderNav() {
16 15 const { lang, setLang } = useLang();
@@ -28,9 +27,9 @@ export default function HeaderNav() {
28 27 };
29 28
30 29 const items = [
31 − { href: "/", label: fr ? "Estimer" : "Estimate" },
30 + { href: "/", label: fr ? "Analyser" : "Analyze" },
32 31 { href: "/carte", label: fr ? "Carte" : "Map" },
33 − { href: "/ka", label: fr ? "Ka · agent IA" : "Ka · AI agent" },
32 + { href: "/assistant", label: fr ? "Assistant IA" : "AI assistant" },
34 33 { href: "/parc", label: fr ? "Parc immobilier" : "Portfolio" },
35 34 { href: "/stats", label: fr ? "Statistiques" : "Statistics" },
36 35 { href: "/methodologie", label: fr ? "Méthodologie" : "Methodology" },
@@ -89,13 +88,14 @@ export default function HeaderNav() {
89 88 <div className="flex h-14 shrink-0 items-center justify-between border-b border-[rgba(245,243,238,0.18)] px-4">
90 89 <Link
91 90 href="/"
92 − className="vp-display flex items-baseline gap-1.5 text-[19px] font-bold leading-none tracking-[-0.04em] text-[var(--paper-on-ink)]"
91 + className="flex items-end gap-1.5"
92 + aria-label="UQO Éval — accueil"
93 93 >
94 − Vrai&nbsp;Prix
95 − <span
96 − aria-hidden="true"
97 − className="inline-block h-[7px] w-[7px] bg-[var(--accent-bright)]"
98 − />
94 + {/* eslint-disable-next-line @next/next/no-img-element */}
95 + <img src="/uqo/uqo-blanc.svg" alt="UQO" className="h-[24px] w-auto" />
96 + <span className="vp-display text-[19px] font-bold leading-none tracking-[-0.04em] text-[var(--accent-bright)]">
97 + Éval
98 + </span>
99 99 </Link>
100 100 <label
101 101 htmlFor="vp-menu"
@@ -138,10 +138,11 @@ export default function HeaderNav() {
138 138
139 139 {/* pied du menu */}
140 140 <div className="shrink-0 border-t border-[rgba(245,243,238,0.18)] px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4">
141 − {/* entrée « Un service Groupe KA » du menu mobile */}
142 − <div className="mb-4">
143 − <GroupeKaBadge />
144 − </div>
141 + <p className="vp-mono mb-4 text-[10px] uppercase tracking-[0.1em] text-[rgba(245,243,238,0.55)]">
142 + {fr
143 + ? "Laboratoire pédagogique d'évaluation immobilière — UQO"
144 + : "Real-estate valuation teaching laboratory — UQO"}
145 + </p>
145 146 <button
146 147 onClick={() => setLang(fr ? "en" : "fr")}
147 148 className="vp-mono mb-4 border-b border-[var(--paper-on-ink)] pb-1 text-[12px] font-medium uppercase tracking-[0.1em] text-[var(--paper-on-ink)]"
modified src/components/KaCarte.tsx +6 −7
@@ -1,7 +1,6 @@
1 1 "use client";
2 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
3 −// Projet : Groupe Ka / Ka Maps (intégration Vrai-Prix)
4 −// components/KaCarte.tsx : Vrai-Prix Maps — recherche par carte propulsée
2 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
3 +// components/KaCarte.tsx : UQO Éval Maps — recherche par carte propulsée
5 4 // par le framework partagé Ka Maps (moteur Mapbox GL, style Standard 3D par
6 5 // défaut + bouton 2D). Remplace l'ancienne carte Leaflet/OSM : pastilles de
7 6 // VALEURS ESTIMÉES 2026 (grappes + moyennes), chargement piloté par le
@@ -26,8 +25,8 @@ import { useLang } from "@/components/LangContext";
26 25 import {
27 26 MAPBOX_TOKEN,
28 27 MIN_ZOOM_FETCH,
29 − vraiPrixMapAdapter,
30 − vraiPrixMapTheme,
28 + uqoEvalMapAdapter,
29 + uqoEvalMapTheme,
31 30 } from "@/lib/kamaps";
32 31
33 32 const DEFAULT_CENTER = { lat: 45.5421, lng: -73.6646 }; // Montréal
@@ -70,8 +69,8 @@ export default function KaCarte() {
70 69 return (
71 70 <div className="vpm-wrap">
72 71 <KaMapView
73 − theme={vraiPrixMapTheme}
74 − adapter={vraiPrixMapAdapter}
72 + theme={uqoEvalMapTheme}
73 + adapter={uqoEvalMapAdapter}
75 74 mapboxToken={MAPBOX_TOKEN}
76 75 center={DEFAULT_CENTER}
77 76 zoom={13.5}
modified src/components/LangContext.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import { createContext, useContext, useEffect, useState } from "react";
4 4 import { t as translate, type Lang, type TKey } from "@/lib/i18n";
deleted src/components/LeadForm.tsx +0 −58
@@ -1,58 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −"use client";
3 −import { useState } from "react";
4 −import { useLang } from "./LangContext";
5 −
6 −export default function LeadForm({
7 − unitId,
8 − estimate,
9 −}: {
10 − unitId: string | null;
11 − estimate: number;
12 −}) {
13 − const { t } = useLang();
14 − const [email, setEmail] = useState("");
15 − const [done, setDone] = useState(false);
16 − const [err, setErr] = useState(false);
17 −
18 − const submit = async (e: React.FormEvent) => {
19 − e.preventDefault();
20 − setErr(false);
21 − const res = await fetch("/api/lead", {
22 − method: "POST",
23 − headers: { "Content-Type": "application/json" },
24 − body: JSON.stringify({ email, unitId, estimate }),
25 − });
26 − if (res.ok) setDone(true);
27 − else setErr(true);
28 − };
29 −
30 − if (done)
31 − return (
32 − <p className="vp-mono border-l-2 border-[var(--accent-bright)] py-1 pl-4 text-[12px] font-bold uppercase tracking-[0.05em] text-[var(--paper-on-ink)]">
33 − ✓ {t("leadOk")}
34 − </p>
35 − );
36 − return (
37 − <form onSubmit={submit} className="flex flex-col gap-3 sm:flex-row sm:gap-0">
38 − <input
39 − type="email"
40 − required
41 − value={email}
42 − onChange={(e) => setEmail(e.target.value)}
43 − placeholder={t("emailPlaceholder")}
44 − className={`min-h-[50px] flex-1 border-0 border-b bg-transparent px-1 py-2.5 text-[16px] text-[var(--paper-on-ink)] outline-none transition-colors placeholder:text-[rgba(245,243,238,0.4)] ${
45 − err
46 − ? "border-[var(--danger)]"
47 − : "border-[rgba(245,243,238,0.45)] focus:border-[var(--accent-bright)]"
48 − }`}
49 − />
50 − <button
51 − type="submit"
52 − className="vp-display flex min-h-[50px] shrink-0 items-center justify-center gap-2 bg-[var(--accent-bright)] px-6 text-[14px] font-bold text-ink transition-colors hover:bg-[#ff6b63]"
53 − >
54 − {t("leadBtn")} →
55 − </button>
56 − </form>
57 − );
58 −}
modified src/components/LegalPage.tsx +3 −3
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import Link from "next/link";
4 4 import { useLang } from "./LangContext";
@@ -30,7 +30,7 @@ export default function LegalPage({
30 30 <div className="py-10">
31 31 <nav className="vp-mono mb-8 flex items-center gap-2 text-[11.5px] uppercase tracking-[0.06em] text-ink-3">
32 32 <Link href="/" className="border-b-[1.5px] border-transparent hover:border-green hover:text-green">
33 − Vrai-Prix
33 + UQO Éval
34 34 </Link>
35 35 <span>/</span>
36 36 <span>{fr ? crumb.fr : crumb.en}</span>
@@ -70,7 +70,7 @@ export default function LegalPage({
70 70 </p>
71 71 <p className="vp-display mt-2 text-[18px] font-bold">Simon-Pierre Boucher</p>
72 72 <p className="vp-mono mt-1 text-[12px] tracking-[0.04em] text-[rgba(245,243,238,0.8)]">
73 − contact@spboucher.ai · vrai-prix.com
73 + contact@spboucher.ai · uqo-eval.app
74 74 </p>
75 75 </div>
76 76 </div>
modified src/components/MetricViz.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * MetricViz — visualisations signature générées par les données du registre.
4 4 * Langage v2 « terminal éditorial » : lignes 2 px, grilles hairline pleines,
modified src/components/MobileTabBar.tsx +2 −2
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Barre de navigation mobile commune Groupe KA (ka-ui/nav — KA Tabbar v1) :
3 + * Barre de navigation mobile commune (tabbar v1) :
4 4 * Estimer · Marché · Ventes · Méthode, accent rouge signature, bilingue.
5 5 * Estimer mène immédiatement à la recherche d'adresse (focus du champ héros) ;
6 6 * Ventes ancre le flux « dernières ventes réelles » de l'accueil.
modified src/components/RadarMap.tsx +2 −2
@@ -1,9 +1,9 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * RadarMap — carte maison « plan d'arpenteur », construite from scratch (SVG pur).
4 4 * Aucune tuile, aucune bibliothèque : le sujet est au centre, chaque comparable
5 5 * est positionné par azimut et distance réels (projection locale équirectangulaire),
6 − * sur des anneaux de distance concentriques. Signature Vrai-Prix : encre, lime,
6 + * sur des anneaux de distance concentriques. Signature UQO Éval : encre, lime,
7 7 * étiquettes mono, grille d'arpentage, flèche nord, échelle.
8 8 */
9 9 "use client";
modified src/components/ResultView.tsx +51 −23
@@ -1,17 +1,17 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * ResultView — le « rapport de valeur » : l'écran central du produit.
3 + * ResultView — le « rapport de valeur » : l'écran central de la plateforme,
4 + * avec MODE ÉTUDIANT (analyse pédagogique complète du calcul).
4 5 * Composition éditoriale v2 : masthead de rapport, estimation en très grands
5 6 * chiffres, sections numérotées à filets (registre, calcul, marché, historique,
6 7 * comparables), zéro carte — la hiérarchie vient de la typographie et des filets.
7 8 */
8 9 "use client";
9 −import { useState } from "react";
10 +import { useEffect, useState } from "react";
10 11 import Link from "next/link";
11 12 import { useLang } from "./LangContext";
12 −import LeadForm from "./LeadForm";
13 13 import RadarMap from "./RadarMap";
14 −import FavButton from "./FavButton";
14 +import StudentPanel from "./StudentPanel";
15 15 import {
16 16 BuildingGlyph,
17 17 ConfidenceDial,
@@ -66,6 +66,24 @@ function Sect({ num, kicker, title }: { num: string; kicker: string; title: stri
66 66 export default function ResultView({ data }: { data: UnitEstimate }) {
67 67 const { lang, t } = useLang();
68 68 const [activeComp, setActiveComp] = useState<string | null>(null);
69 + // mode étudiant — persiste sur l'appareil (localStorage)
70 + const [student, setStudent] = useState(false);
71 + useEffect(() => {
72 + const id = setTimeout(() => {
73 + try {
74 + setStudent(window.localStorage.getItem("uqo-eval-student") === "1");
75 + } catch {}
76 + }, 0);
77 + return () => clearTimeout(id);
78 + }, []);
79 + const toggleStudent = () => {
80 + setStudent((v) => {
81 + try {
82 + window.localStorage.setItem("uqo-eval-student", v ? "0" : "1");
83 + } catch {}
84 + return !v;
85 + });
86 + };
69 87 const r: EstimateResult = data.result;
70 88 const u = data.unit;
71 89 const ctx = data.context;
@@ -86,7 +104,7 @@ export default function ResultView({ data }: { data: UnitEstimate }) {
86 104 <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b-2 border-ink pb-2.5">
87 105 <nav className="vp-mono flex min-w-0 flex-wrap items-center gap-2 text-[10.5px] uppercase tracking-[0.08em] text-ink-3">
88 106 <Link href="/" className="text-ink-2 hover:text-accent-deep">
89 − Vrai-Prix
107 + UQO&nbsp;Éval
90 108 </Link>
91 109 <span aria-hidden="true">/</span>
92 110 <span>{fr ? "Rapport de valeur" : "Value report"}</span>
@@ -97,18 +115,7 @@ export default function ResultView({ data }: { data: UnitEstimate }) {
97 115 </>
98 116 )}
99 117 </nav>
100 − {u && (
101 − <FavButton
102 − size="lg"
103 − item={{
104 − item_id: u.id,
105 − title: `${u.adresse ?? u.id}${s?.apt ? ` app. ${s.apt}` : ""}`,
106 − subtitle: u.municipalite ?? "",
107 − price_label: r.estimate > 0 ? moneyFmt(r.estimate) : "",
108 − url: `https://www.vrai-prix.com/estimation/${encodeURIComponent(u.id)}`,
109 − }}
110 − />
111 − )}
118 +
112 119 </div>
113 120
114 121 <div className="mt-6 grid gap-x-12 gap-y-8 lg:grid-cols-[1.5fr_1fr]">
@@ -154,6 +161,14 @@ export default function ResultView({ data }: { data: UnitEstimate }) {
154 161 </div>
155 162 {u && (
156 163 <div className="mt-7 flex flex-wrap gap-2.5">
164 + <button
165 + type="button"
166 + onClick={toggleStudent}
167 + className={student ? "btn btn-accent" : "btn btn-ghost"}
168 + aria-pressed={student}
169 + >
170 + 🎓 {t("studentMode")} — {student ? t("hideAnalysis") : t("seeAnalysis")}
171 + </button>
157 172 <a
158 173 href={`/api/report?id=${encodeURIComponent(u.id)}`}
159 174 className="btn btn-primary"
@@ -342,6 +357,9 @@ export default function ResultView({ data }: { data: UnitEstimate }) {
342 357 </div>
343 358 </section>
344 359
360 + {/* ================= analyse pédagogique (mode étudiant) ================= */}
361 + {student && <StudentPanel r={r} />}
362 +
345 363 {/* ================= 03 — marché local ================= */}
346 364 {ctx && (ctx.index.length >= 6 || ctx.sector || ctx.muniPercentile != null) && (
347 365 <section>
@@ -601,17 +619,27 @@ export default function ResultView({ data }: { data: UnitEstimate }) {
601 619 </div>
602 620 </section>
603 621
604 − {/* ================= alerte de valeur ================= */}
622 + {/* ================= poursuivre l'apprentissage ================= */}
605 623 <section className="bleed vp-dark">
606 624 <div className="mx-auto max-w-6xl px-4 py-10 sm:px-6 sm:py-12">
607 625 <span className="sec-num !text-[var(--accent-bright)]">
608 − 06 — {fr ? "Suivi" : "Watch"}
626 + 06 — {fr ? "Aller plus loin" : "Go further"}
609 627 </span>
610 628 <h2 className="vp-display mt-2 text-[clamp(20px,3vw,28px)] font-bold uppercase tracking-[-0.025em] text-[var(--paper-on-ink)]">
611 − {t("leadTitle")}
629 + {fr ? "Poursuivre l'apprentissage" : "Keep learning"}
612 630 </h2>
613 − <div className="mt-5 max-w-xl">
614 − <LeadForm unitId={u?.id ?? null} estimate={r.estimate} />
631 + <p className="mt-3 max-w-2xl text-[14px] leading-relaxed text-[rgba(233,237,238,0.72)]">
632 + {fr
633 + ? "La méthodologie complète — données, fusion spatiale, modèle hédonique, validation IAAO, méthodes d'évaluation — est publiée intégralement. Éva, l'assistante pédagogique, peut aussi expliquer chaque chiffre de cette page."
634 + : "The full methodology — data, spatial merge, hedonic model, IAAO validation, appraisal methods — is published in full. Éva, the teaching assistant, can also explain every figure on this page."}
635 + </p>
636 + <div className="mt-6 flex flex-wrap gap-3">
637 + <Link href="/methodologie" className="btn btn-accent">
638 + {fr ? "Lire la méthodologie" : "Read the methodology"} →
639 + </Link>
640 + <Link href="/assistant" className="btn !border-[rgba(233,237,238,0.5)] !text-[var(--paper-on-ink)] hover:!border-[var(--accent-bright)]">
641 + {fr ? "Demander à Éva" : "Ask Éva"} →
642 + </Link>
615 643 </div>
616 644 </div>
617 645 </section>
modified src/components/SearchBox.tsx +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 "use client";
3 3 import { useRef, useState } from "react";
4 4 import { useRouter } from "next/navigation";
modified src/components/StatsDashboard.tsx +2 −2
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Tableau de bord /stats v2 — module Stats commun Groupe KA (src/ka/stats/
3 + * Tableau de bord /stats v2 — module Stats commun (src/ka/stats/
4 4 * SPEC.md §1), monté sur le kit kacharts : bandeau KPI (sparklines), jauges,
5 5 * courbes + stats de séries, multi-courbes, barres empilées, répartitions,
6 6 * distributions, géographie, calendrier d'activité, tableaux, records,
modified src/components/StatsView.tsx +4 −4
@@ -1,9 +1,9 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * /stats — bandeau « LE CHIFFRE » (valeur totale du parc immobilier
4 4 * québécois, comparaisons vulgarisées). Les sections analytiques
5 5 * (KPI, jauges, séries, répartitions, tableaux, records, rapports PDF)
6 − * sont servies par <StatsDashboard/> — module Stats commun Groupe KA v2.
6 + * sont servies par <StatsDashboard/> — module Stats commun v2.
7 7 */
8 8 "use client";
9 9 import { CountUp } from "./MetricViz";
@@ -101,8 +101,8 @@ export default function StatsView({ s }: { s: ProvStats }) {
101 101 </div>
102 102 <p className="vp-mono mt-3 text-[10.5px] uppercase tracking-[0.06em] text-ink-3">
103 103 {fr
104 − ? "Somme des 3 747 008 estimations Vrai-Prix (modèle hédonique, rôle 2026). Évaluation municipale totale correspondante : "
105 − : "Sum of 3,747,008 Vrai-Prix estimates (hedonic model, 2026 roll). Corresponding total municipal assessment: "}
104 + ? "Somme des 3 747 008 estimations UQO Éval (modèle hédonique, rôle 2026). Évaluation municipale totale correspondante : "
105 + : "Sum of 3,747,008 UQO Éval estimates (hedonic model, 2026 roll). Corresponding total municipal assessment: "}
106 106 {compact(s.valeur_role_totale, lang)}.
107 107 </p>
108 108 </section>
added src/components/StudentPanel.tsx +232 −0
@@ -0,0 +1,232 @@
1 +// UQO Éval — mode étudiant : l'analyse pédagogique complète d'une estimation.
2 +/**
3 + * Décompose le résultat du moteur (engine.ts) SANS inventer de calcul :
4 + * chaque chiffre affiché est recomposé à partir des mêmes règles que le
5 + * moteur (sélection des comparables, gaussiennes de pondération, ajustements,
6 + * combinaison 65/35, score de confiance) appliquées aux données du résultat.
7 + */
8 +"use client";
9 +import { useLang } from "./LangContext";
10 +import type { EstimateResult } from "@/lib/engine";
11 +
12 +const money = (v: number, lang: string) =>
13 + new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", {
14 + style: "currency",
15 + currency: "CAD",
16 + maximumFractionDigits: 0,
17 + }).format(Math.round(v));
18 +
19 +/** Reconstruit la décomposition du score de confiance (mêmes règles que engine.ts). */
20 +function confidenceParts(r: EstimateResult) {
21 + const base = 35;
22 + const comps = Math.min(25, r.comps.length * 2.5);
23 + const disp =
24 + r.compsDispersionPct != null ? Math.max(0, 20 - r.compsDispersionPct) : 0;
25 + const model = r.modelEstimate != null ? 15 : 0;
26 + const widthPct = r.estimate > 0 ? ((r.high - r.low) / r.estimate) * 100 : 100;
27 + const width = -Math.max(0, (widthPct - 30) / 3);
28 + return { base, comps, disp, model, width, widthPct };
29 +}
30 +
31 +function Formula({ children }: { children: React.ReactNode }) {
32 + return (
33 + <p className="vp-mono mt-2 overflow-x-auto whitespace-nowrap border-l-2 border-accent bg-surface px-3 py-2 text-[11.5px] text-ink">
34 + {children}
35 + </p>
36 + );
37 +}
38 +
39 +function Block({
40 + title,
41 + children,
42 +}: {
43 + title: string;
44 + children: React.ReactNode;
45 +}) {
46 + return (
47 + <div className="border-t border-[var(--line)] py-5 first:border-t-0 first:pt-0">
48 + <h3 className="vp-display text-[15px] font-bold uppercase tracking-[-0.01em]">
49 + {title}
50 + </h3>
51 + <div className="mt-2 max-w-3xl text-[13.5px] leading-relaxed text-ink-2">
52 + {children}
53 + </div>
54 + </div>
55 + );
56 +}
57 +
58 +export default function StudentPanel({ r }: { r: EstimateResult }) {
59 + const { lang, t } = useLang();
60 + const fr = lang === "fr";
61 + const parts = confidenceParts(r);
62 + const total = Math.max(
63 + 5,
64 + Math.min(97, parts.base + parts.comps + parts.disp + parts.model + parts.width)
65 + );
66 + const modelPct = Math.round(r.modelWeight * 100);
67 + const compsPct = 100 - modelPct;
68 +
69 + const confRows: [string, number][] = [
70 + [t("pedConfBase"), parts.base],
71 + [t("pedConfComps"), parts.comps],
72 + [t("pedConfDisp"), parts.disp],
73 + [t("pedConfModel"), parts.model],
74 + [t("pedConfWidth"), parts.width],
75 + ];
76 +
77 + return (
78 + <section className="rise border border-[var(--line-strong)] bg-surface-2 p-5 sm:p-8">
79 + <header className="flex flex-wrap items-baseline justify-between gap-2 border-b-2 border-ink pb-3">
80 + <h2 className="vp-display text-[clamp(18px,2.6vw,24px)] font-bold uppercase tracking-[-0.02em]">
81 + {fr ? "Analyse pédagogique" : "Pedagogical analysis"}
82 + </h2>
83 + <span className="vp-mono text-[10px] uppercase tracking-[0.1em] text-accent-deep">
84 + {t("studentMode")} · IMM1003 / IMM1033
85 + </span>
86 + </header>
87 + <p className="mt-4 max-w-3xl text-[13.5px] leading-relaxed text-ink-2">
88 + {t("pedIntro")}
89 + </p>
90 +
91 + <div className="mt-6">
92 + {/* 1 — sélection des comparables */}
93 + <Block title={t("pedWhyCompsTitle")}>
94 + <p>{t("pedWhyComps")}</p>
95 + <p className="vp-mono mt-2 text-[11px] uppercase tracking-[0.05em] text-ink-3">
96 + {fr
97 + ? `Ici : ${r.nCompsUsed} comparables retenus${r.compsDispersionPct != null ? ` · dispersion médiane ±${r.compsDispersionPct} %` : ""}`
98 + : `Here: ${r.nCompsUsed} comparables kept${r.compsDispersionPct != null ? ` · median dispersion ±${r.compsDispersionPct}%` : ""}`}
99 + </p>
100 + </Block>
101 +
102 + {/* 2 — pondération */}
103 + <Block title={t("pedWeightTitle")}>
104 + <p>{t("pedWeight")}</p>
105 + <Formula>
106 + poids = exp(−(distance∕1500&nbsp;m)²) × exp(−(mois∕24)²) ×
107 + exp(−(Δaire∕25&nbsp;%)²)
108 + </Formula>
109 + </Block>
110 +
111 + {/* 3 — ajustements */}
112 + <Block title={t("pedAdjTitle")}>
113 + <ul className="list-none space-y-2 p-0">
114 + <li>
115 + <b className="text-ink">{t("adjTime")}</b> — {t("pedAdjTime")}
116 + </li>
117 + <li>
118 + <b className="text-ink">{t("adjArea")}</b> — {t("pedAdjArea")}
119 + </li>
120 + <li>
121 + <b className="text-ink">{t("adjAge")}</b> — {t("pedAdjAge")}
122 + </li>
123 + </ul>
124 + <Formula>
125 + prix ajusté = prix de vente + ajust. marché + ajust. superficie +
126 + ajust. âge
127 + </Formula>
128 + </Block>
129 +
130 + {/* 4 — combinaison finale */}
131 + <Block title={t("pedBlendTitle")}>
132 + <p>{t("pedBlend")}</p>
133 + {/* barre de combinaison réelle */}
134 + <div className="mt-3 max-w-xl">
135 + <div className="flex h-8 overflow-hidden border border-[var(--line-strong)]">
136 + {r.modelEstimate != null && modelPct > 0 && (
137 + <span
138 + className="flex items-center justify-center bg-ink text-[10.5px] font-bold text-paper"
139 + style={{ width: `${modelPct}%` }}
140 + >
141 + {modelPct} %
142 + </span>
143 + )}
144 + {r.compsEstimate != null && compsPct > 0 && (
145 + <span
146 + className="flex items-center justify-center bg-accent text-[10.5px] font-bold text-white"
147 + style={{ width: `${compsPct}%` }}
148 + >
149 + {compsPct} %
150 + </span>
151 + )}
152 + </div>
153 + <p className="vp-mono mt-1.5 flex flex-wrap justify-between gap-2 text-[10px] uppercase tracking-[0.06em] text-ink-3">
154 + <span>
155 + {t("modelPart")} :{" "}
156 + {r.modelEstimate != null ? money(r.modelEstimate, lang) : "—"}
157 + </span>
158 + <span>
159 + {t("compsPart")} :{" "}
160 + {r.compsEstimate != null ? money(r.compsEstimate, lang) : "—"}
161 + </span>
162 + </p>
163 + </div>
164 + {r.modelEstimate != null && r.compsEstimate != null && (
165 + <Formula>
166 + {(r.modelWeight).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 2 })}
167 + &nbsp;× {money(r.modelEstimate, lang)} +{" "}
168 + {(1 - r.modelWeight).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 2 })}
169 + &nbsp;× {money(r.compsEstimate, lang)} ≈ {money(r.estimate, lang)}
170 + </Formula>
171 + )}
172 + </Block>
173 +
174 + {/* 5 — indice de confiance décomposé */}
175 + <Block title={t("pedConfTitle")}>
176 + <div className="max-w-xl">
177 + {confRows.map(([label, v]) => (
178 + <div
179 + key={label}
180 + className="flex items-baseline justify-between gap-4 border-b border-[var(--line-soft)] py-1.5"
181 + >
182 + <span className="text-[13px]">{label}</span>
183 + <span
184 + className={`vp-mono text-[12.5px] font-bold ${v < 0 ? "text-[var(--danger)]" : "text-ink"}`}
185 + >
186 + {v >= 0 ? "+" : "−"}
187 + {Math.abs(v).toFixed(1).replace(/\.0$/, "")}
188 + </span>
189 + </div>
190 + ))}
191 + <div className="flex items-baseline justify-between gap-4 border-t-2 border-ink py-2">
192 + <span className="vp-display text-[14px] font-bold uppercase">
193 + {t("pedConfTotal")}
194 + </span>
195 + <span className="vp-mono text-[14px] font-bold text-ink">
196 + {Math.round(total)} / 100 → {r.confidenceLevel}
197 + </span>
198 + </div>
199 + <p className="vp-mono mt-1 text-[10px] uppercase tracking-[0.05em] text-ink-3">
200 + {fr
201 + ? "Niveaux : A ≥ 75 · B ≥ 60 · C ≥ 45 · D < 45 (borné à 5-97)"
202 + : "Levels: A ≥ 75 · B ≥ 60 · C ≥ 45 · D < 45 (clamped 5-97)"}
203 + </p>
204 + </div>
205 + </Block>
206 +
207 + {/* 6 — concepts clés */}
208 + <Block title={t("pedConceptsTitle")}>
209 + <div className="grid gap-x-8 gap-y-4 sm:grid-cols-3">
210 + {(
211 + [
212 + ["pedConceptMarket", "pedConceptMarketDef"],
213 + ["pedConceptAsking", "pedConceptAskingDef"],
214 + ["pedConceptSale", "pedConceptSaleDef"],
215 + ] as const
216 + ).map(([k, d]) => (
217 + <div key={k}>
218 + <p className="vp-mono text-[11px] font-bold uppercase tracking-[0.08em] text-accent-deep">
219 + {t(k)}
220 + </p>
221 + <p className="mt-1 text-[13px] leading-snug">{t(d)}</p>
222 + </div>
223 + ))}
224 + </div>
225 + <p className="mt-4">
226 + <b className="text-ink">{t("pedRoleTitle")}</b> {t("pedRole")}
227 + </p>
228 + </Block>
229 + </div>
230 + </section>
231 + );
232 +}
added src/components/UqoFooter.tsx +87 −0
@@ -0,0 +1,87 @@
1 +// UQO Éval — pied de page institutionnel (fond encre, logo officiel UQO blanc).
2 +"use client";
3 +import Link from "next/link";
4 +import { useLang } from "./LangContext";
5 +
6 +export default function UqoFooter() {
7 + const { lang } = useLang();
8 + const fr = lang === "fr";
9 + const year = new Date().getFullYear();
10 + return (
11 + <footer className="ka-footer" id="contact">
12 + <div className="container">
13 + {/* logo officiel UQO (version blanche, proportions natives) + wordmark */}
14 + <div className="flex items-end gap-3">
15 + {/* eslint-disable-next-line @next/next/no-img-element */}
16 + <img
17 + src="/uqo/uqo-blanc.svg"
18 + alt="UQO — Université du Québec en Outaouais"
19 + className="h-9 w-auto"
20 + />
21 + <span className="vp-display text-[30px] font-bold leading-none tracking-[-0.04em] text-[var(--accent-bright)]">
22 + Éval
23 + </span>
24 + </div>
25 + <p className="desc">
26 + {fr ? (
27 + <>
28 + <b style={{ color: "var(--paper)" }}>UQO Éval</b> — laboratoire
29 + pédagogique d&apos;évaluation immobilière. La plateforme sert à
30 + explorer les mécanismes de l&apos;évaluation : données ouvertes du
31 + rôle foncier québécois (MAMH), ventes réelles, modèle hédonique et
32 + comparables ajustés — chaque calcul montré en entier.
33 + </>
34 + ) : (
35 + <>
36 + <b style={{ color: "var(--paper)" }}>UQO Éval</b> — a teaching
37 + laboratory for real-estate valuation. The platform explores the
38 + mechanics of appraisal: Québec open assessment-roll data (MAMH),
39 + real sales, a hedonic model and adjusted comparables — every
40 + computation shown in full.
41 + </>
42 + )}
43 + </p>
44 + <p className="notice">
45 + <b>
46 + {fr
47 + ? "Plateforme à vocation pédagogique, universitaire et expérimentale — les estimations ne constituent pas une évaluation immobilière professionnelle certifiée (OEAQ)."
48 + : "Educational, academic and experimental platform — estimates are not a certified professional appraisal (OEAQ)."}
49 + </b>
50 + </p>
51 + <p className="desc" style={{ marginTop: "var(--sp-3)", fontSize: 12.5 }}>
52 + {fr
53 + ? "Développée dans le cadre des cours IMM1003 — Éléments d'évaluation immobilière et IMM1033 — Méthodes du coût en évaluation immobilière, enseignés par Simon-Pierre Boucher (Université du Québec en Outaouais)."
54 + : "Developed for courses IMM1003 — Fundamentals of Real-Estate Appraisal and IMM1033 — Cost Approaches in Real-Estate Appraisal, taught by Simon-Pierre Boucher (Université du Québec en Outaouais)."}
55 + </p>
56 + <ul className="sites">
57 + <li>
58 + <Link href="/methodologie">{fr ? "Méthodologie" : "Methodology"}</Link>
59 + </li>
60 + <li>
61 + <Link href="/stats">{fr ? "Statistiques" : "Statistics"}</Link>
62 + </li>
63 + <li>
64 + <Link href="/municipalites">{fr ? "Municipalités" : "Municipalities"}</Link>
65 + </li>
66 + <li>
67 + <Link href="/carte">{fr ? "Carte" : "Map"}</Link>
68 + </li>
69 + <li>
70 + <a href="https://uqo.ca" target="_blank" rel="noopener noreferrer">
71 + uqo.ca
72 + </a>
73 + </li>
74 + </ul>
75 + <p className="legal">
76 + © {year} UQO Éval — {fr ? "laboratoire pédagogique" : "teaching laboratory"}
77 + {" · "}
78 + <Link href="/conditions">{fr ? "Conditions" : "Terms"}</Link>
79 + {" · "}
80 + <Link href="/confidentialite">{fr ? "Confidentialité" : "Privacy"}</Link>
81 + {" · "}
82 + <Link href="/contact">Contact</Link>
83 + </p>
84 + </div>
85 + </footer>
86 + );
87 +}
deleted src/ka/GroupeKaBadge.tsx +0 −21
@@ -1,21 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// ka-ui/react/GroupeKaBadge.tsx — badge « Un service Groupe KA », cliquable
3 −// vers le hub. À placer dans le header de chaque site (à côté du logo de la
4 −// marque). Variante dark pour les surfaces encre. Zone de protection : le
5 −// padding interne du badge suffit ; ne rien coller à moins de 8 px du badge.
6 −export default function GroupeKaBadge({ dark = false }: { dark?: boolean }) {
7 − return (
8 − <a
9 − className={`gk-badge${dark ? " gk-badge--dark" : ""}`}
10 − href="https://www.groupe-ka.com"
11 − target="_blank"
12 − rel="noopener noreferrer"
13 − aria-label="Un service Groupe KA — visiter le portail de l'écosystème"
14 − >
15 − Un service
16 − <b>
17 − Groupe<span className="ka">KA</span>
18 − </b>
19 − </a>
20 − );
21 −}
deleted src/ka/KaFooter.tsx +0 −91
@@ -1,91 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// ka-ui/react/KaFooter.tsx — footer commun Groupe KA (fond encre), à vendorer
3 −// dans chaque app Vite/React sous frontend/src/ka/. Consomme ecosystem.json :
4 −// toute modification des infos de contact se fait dans ka-ui/ecosystem.json
5 −// puis se resynchronise sur les 12 sites (voir ka-ui/README.md).
6 −import eco from "./ecosystem.json";
7 −
8 −type LocalLink = { label: string; href: string };
9 −
10 −export default function KaFooter({
11 − siteId,
12 − localLegal = [],
13 −}: {
14 − /** id du site courant dans ecosystem.json (ex. "resto-ka") */
15 − siteId: string;
16 − /** pages légales PROPRES au site (ex. /conditions), affichées avant celles du hub */
17 − localLegal?: LocalLink[];
18 −}) {
19 − const year = new Date().getFullYear();
20 − const site = eco.sites.find((s) => s.id === siteId);
21 − const others = eco.sites.filter((s) => s.id !== siteId);
22 − return (
23 − <footer className="ka-footer" id="contact">
24 − <div className="container">
25 − <a
26 − className="wordmark"
27 − href={eco.hub.url}
28 − target={siteId === "groupe-ka" ? undefined : "_blank"}
29 − rel="noopener noreferrer"
30 − aria-label="Groupe KA — le portail de l'écosystème"
31 − >
32 − Groupe <span className="ka">KA</span>
33 − </a>
34 − <p className="desc">
35 − {eco.org.tagline} Des centaines de connecteurs lisent les sites à la
36 − source, normalisent la donnée et se resynchronisent seuls.{" "}
37 − {site ? (
38 − <>
39 − <b style={{ color: "var(--paper)" }}>{site.wordmark}</b> —{" "}
40 − {site.tagline} — est un service Groupe KA.
41 − </>
42 − ) : null}
43 − </p>
44 − <p className="notice">
45 − <b>{eco.org.disclaimer}</b>
46 − </p>
47 − <ul className="sites">
48 − {others.map((s) => (
49 − <li key={s.id}>
50 − <a href={`https://${s.domain}`} target="_blank" rel="noopener noreferrer">
51 − {s.wordmark}
52 − </a>
53 − </li>
54 − ))}
55 − {eco.extraFooterLinks.map((l) => (
56 − <li key={l.href}>
57 − <a href={l.href} target="_blank" rel="noopener noreferrer">
58 − {l.label}
59 − </a>
60 − </li>
61 − ))}
62 − </ul>
63 − <div className="contacts">
64 − {eco.contacts.map((c) => (
65 − <p key={c.email} style={{ margin: 0 }}>
66 − <a href={`mailto:${c.email}`}>{c.email}</a>
67 − <span>{c.role}</span>
68 − </p>
69 − ))}
70 − </div>
71 − <p className="legal">
72 − © {year} {eco.org.copyrightHolder}
73 − {localLegal.map((l) => (
74 − <span key={l.href}>
75 − {" · "}
76 − <a href={l.href}>{l.label}</a>
77 − </span>
78 − ))}
79 − {eco.legal.map((l) => (
80 − <span key={l.href}>
81 − {" · "}
82 − <a href={l.href} target="_blank" rel="noopener noreferrer">
83 − {l.label}
84 − </a>
85 − </span>
86 − ))}
87 − </p>
88 − </div>
89 − </footer>
90 − );
91 −}
deleted src/ka/ecosystem.json +0 −207
@@ -1,207 +0,0 @@
1 −{
2 − "org": {
3 − "name": "Groupe KA",
4 − "legalName": "Groupe KA — Simon-Pierre Boucher",
5 − "tagline": "Holding québécois d'agrégateurs de produits et services entièrement automatisés.",
6 − "disclaimer": "Groupe KA est un agrégateur de contenu : nous ne vendons rien, ne louons rien et ne sommes partie à aucune transaction.",
7 − "copyrightHolder": "Groupe KA — Simon-Pierre Boucher"
8 − },
9 − "hub": {
10 − "url": "https://www.groupe-ka.com",
11 − "loginPath": "/connexion",
12 − "signupNote": "La création de compte KA ID se fait sur le hub groupe-ka.com ; chaque site délègue sa connexion via /api/auth/ka/login."
13 − },
14 − "contacts": [
15 − {
16 − "email": "contact@groupe-ka.com",
17 − "role": "Projets, partenariats & données"
18 − },
19 − {
20 − "email": "info@groupe-ka.com",
21 − "role": "Médias & questions générales"
22 − },
23 − {
24 − "email": "admin@groupe-ka.com",
25 − "role": "Légal, vie privée & Loi 25"
26 − }
27 − ],
28 − "legal": [
29 − {
30 − "label": "Conditions d'utilisation",
31 − "href": "https://www.groupe-ka.com/conditions"
32 − },
33 − {
34 − "label": "Politique de confidentialité",
35 − "href": "https://www.groupe-ka.com/confidentialite"
36 − },
37 − {
38 − "label": "Protection des renseignements personnels (Loi 25)",
39 − "href": "https://www.groupe-ka.com/loi-25"
40 − },
41 − {
42 − "label": "Transparence des robots d'indexation",
43 − "href": "https://www.groupe-ka.com/bots"
44 − }
45 − ],
46 − "sites": [
47 − {
48 − "id": "groupe-ka",
49 − "wordmark": "Groupe KA",
50 − "domain": "www.groupe-ka.com",
51 − "accent": "#d9f26b",
52 − "accentSoft": "#f0f9d2",
53 − "accentDeep": "#123f2e",
54 − "onAccent": "#141814",
55 − "tagline": "Le portail de l'écosystème ·Ka"
56 − },
57 − {
58 − "id": "trouve-ka",
59 − "wordmark": "Trouve·Ka",
60 − "domain": "www.trouve-ka.com",
61 − "accent": "#1c7ed6",
62 − "accentSoft": "#e7f2fd",
63 − "accentDeep": "#14508f",
64 − "onAccent": "#ffffff",
65 − "tagline": "Le moteur de recherche du web québécois"
66 − },
67 − {
68 − "id": "lou-ka",
69 − "wordmark": "Lou·Ka",
70 − "domain": "www.lou-ka.com",
71 − "accent": "#ff6a00",
72 − "accentSoft": "#fff1e6",
73 − "accentDeep": "#cc5500",
74 − "onAccent": "#ffffff",
75 − "tagline": "Tous les logements à louer"
76 − },
77 − {
78 − "id": "immo-ka",
79 − "wordmark": "Immo·Ka",
80 − "domain": "www.immo-ka.com",
81 − "accent": "#e23744",
82 − "accentSoft": "#fbe0e2",
83 − "accentDeep": "#a8232e",
84 − "onAccent": "#ffffff",
85 − "tagline": "Toutes les propriétés à vendre"
86 − },
87 − {
88 − "id": "vrai-prix",
89 − "wordmark": "Vrai-Prix",
90 − "domain": "www.vrai-prix.com",
91 − "accent": "#ff5148",
92 − "accentSoft": "#ffe3e0",
93 − "accentDeep": "#9e2a25",
94 − "onAccent": "#ffffff",
95 − "tagline": "La valeur réelle de chaque propriété"
96 − },
97 − {
98 − "id": "auto-ka",
99 − "wordmark": "Auto·Ka",
100 − "domain": "www.auto-ka.com",
101 − "accent": "#ff5a2a",
102 − "accentSoft": "#ffe8de",
103 − "accentDeep": "#cc3f16",
104 − "onAccent": "#ffffff",
105 − "tagline": "Les voitures usagées du Québec"
106 − },
107 − {
108 − "id": "fabri-ka",
109 − "wordmark": "Fabri·Ka",
110 − "domain": "www.fabri-ka.com",
111 − "accent": "#c4532e",
112 − "accentSoft": "#f7e3da",
113 − "accentDeep": "#a94525",
114 − "onAccent": "#ffffff",
115 − "tagline": "Les produits fabriqués au Québec"
116 − },
117 − {
118 − "id": "food-ka",
119 − "wordmark": "Food·Ka",
120 − "domain": "www.food-ka.com",
121 − "accent": "#1f9d55",
122 − "accentSoft": "#e2f5ea",
123 − "accentDeep": "#157a40",
124 − "onAccent": "#ffffff",
125 − "tagline": "Les prix d'épicerie, suivis à la source"
126 − },
127 − {
128 − "id": "resto-ka",
129 − "wordmark": "Resto·Ka",
130 − "domain": "www.resto-ka.com",
131 − "accent": "#f08c00",
132 − "accentSoft": "#fdeed7",
133 − "accentDeep": "#b96a00",
134 − "onAccent": "#141814",
135 − "tagline": "Chaque resto, chaque plat, chaque prix"
136 − },
137 − {
138 − "id": "sorti-ka",
139 − "wordmark": "Sorti·Ka",
140 − "domain": "www.sorti-ka.com",
141 − "accent": "#d6336c",
142 − "accentSoft": "#fbe0eb",
143 − "accentDeep": "#a12551",
144 − "onAccent": "#ffffff",
145 − "tagline": "Toutes les sorties, dans les 17 régions"
146 − },
147 − {
148 − "id": "crea-ka",
149 − "wordmark": "Créa·Ka",
150 − "domain": "www.crea-ka.com",
151 − "accent": "#7048e8",
152 − "accentSoft": "#ece5fc",
153 − "accentDeep": "#5433b8",
154 − "onAccent": "#ffffff",
155 − "tagline": "Les créateurs d'ici, tous leurs liens"
156 − },
157 − {
158 − "id": "api-ka",
159 − "wordmark": "API·Ka",
160 − "domain": "www.api-ka.com",
161 − "accent": "#3b5bdb",
162 − "accentSoft": "#e4eafb",
163 − "accentDeep": "#2b44a8",
164 − "onAccent": "#ffffff",
165 − "tagline": "La donnée de l'écosystème, par API"
166 − },
167 − {
168 − "id": "job-ka",
169 − "wordmark": "Job·Ka",
170 − "domain": "www.job-ka.com",
171 − "accent": "#0c8599",
172 − "accentSoft": "#def0f4",
173 − "accentDeep": "#095c6b",
174 − "onAccent": "#ffffff",
175 − "tagline": "Tous les emplois des employeurs québécois"
176 − }
177 − ,
178 − {
179 − "id": "ka-stats",
180 − "wordmark": "Ka·Stats",
181 − "domain": "www.ka-stats.com",
182 − "accent": "#095797",
183 − "accentSoft": "#e2edf6",
184 − "accentDeep": "#063a63",
185 − "onAccent": "#ffffff",
186 − "tagline": "L'explorateur de statistiques du Québec"
187 − }
188 − ],
189 − "extraFooterLinks": [
190 − {
191 − "label": "ValoPlex",
192 − "href": "https://www.valoplex.com"
193 − },
194 − {
195 − "label": "Ka2",
196 − "href": "https://www.ka2.bot"
197 − },
198 − {
199 − "label": "Ka4",
200 − "href": "https://www.ka4.bot"
201 − },
202 − {
203 − "label": "Ka6",
204 − "href": "https://www.ka6.bot"
205 − }
206 − ]
207 −}
\ No newline at end of file
modified src/ka/stats/kacharts.tsx +9 −4
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // ka-ui/stats/kacharts.tsx — kit de graphiques SVG du module Stats commun
3 −// Groupe KA (zéro dépendance, React 18+). Style : design system ka-ui
3 +// (zéro dépendance, React 18+). Style : design system « éditorial sharp »
4 4 // (bordures encre, accent de la plateforme via var(--accent)).
5 5 // v2 — composants : KpiCard (+sparkline), PeriodSelector, LineChart (ligne/
6 6 // aire, infobulle + légende cliquable + comparaison N-1), MultiLineChart
@@ -449,7 +449,12 @@ export function Donut({ title, items }: { title: string; items: BreakItem[] }) {
449 449 const total = rows.reduce((s, r) => s + r.value, 0);
450 450 if (!total) return <EmptyBlock title={title} />;
451 451 const R = 74, C = 2 * Math.PI * R;
452 − let acc = 0;
452 + // offsets cumulés pré-calculés (pas de réassignation pendant le rendu)
453 + const offsets: number[] = [];
454 + rows.reduce((acc, r) => {
455 + offsets.push(acc);
456 + return acc + r.value / total;
457 + }, 0);
453 458 const shades = [1, 0.78, 0.58, 0.42, 0.3, 0.22, 0.15, 0.1];
454 459 return (
455 460 <figure className="card" style={{ margin: 0, padding: 16 }}>
@@ -458,7 +463,7 @@ export function Donut({ title, items }: { title: string; items: BreakItem[] }) {
458 463 <svg viewBox="0 0 200 200" style={{ width: 180, maxWidth: "100%" }} role="img" aria-label={title}>
459 464 {rows.map((r, i) => {
460 465 const frac = r.value / total;
461 − const off = acc; acc += frac;
466 + const off = offsets[i];
462 467 return (
463 468 <circle key={r.label} cx={100} cy={100} r={R} fill="none"
464 469 stroke="var(--accent)" strokeOpacity={shades[i % shades.length]}
modified src/ka/stats/kapdf.py +1 −1
@@ -1,4 +1,4 @@
1 −# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +# UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 # ka-ui/stats/kapdf.py — moteur PDF commun Groupe KA (fpdf2). v3
3 3 # Consomme le JSON du contrat /api/stats/dashboard (voir SPEC.md) et produit
4 4 # les rapports estampillés Groupe-KA. 5 modes fixes :
modified src/ka/tokens.css +1 −1
@@ -1,7 +1,7 @@
1 1 /* -----------------------------------------------------------------------------
2 2 Auteur : Simon-Pierre Boucher — contact@spboucher.ai
3 3 Fichier : ka-ui/tokens.css — SOURCE CANONIQUE (repo ka-ui sur spbgit)
4 − Desc. : Design system commun GROUPE KA « éditorial sharp » — tokens + socle
4 + Desc. : Design system « éditorial sharp » — tokens + socle
5 5 de composants partagés par les 12 plateformes. Chaque site importe ce
6 6 fichier PUIS surcharge uniquement son accent (voir accents.css).
7 7
modified src/lib/db.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import Database from "better-sqlite3";
3 3 import path from "path";
4 4
modified src/lib/engine.test.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 import { describe, expect, it } from "vitest";
3 3 import {
4 4 adjustComps,
modified src/lib/engine.ts +2 −2
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Moteur d'estimation Vrai-Prix — transparent par conception.
3 + * Moteur d'estimation UQO Éval — transparent par conception.
4 4 *
5 5 * Deux sources combinées :
6 6 * 1. Modèle hédonique (LightGBM, entraîné sur ~690 k ventes QC 2021-2026,
modified src/lib/estimator.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Service d'estimation : relie la base SQLite au moteur (engine.ts).
3 3 import {
4 4 getCandidates,
deleted src/lib/hub-profile.ts +0 −94
@@ -1,94 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// -----------------------------------------------------------------------------
3 −// Profil membre lu depuis le HUB Groupe KA (groupe-ka.com).
4 −// Le hub est LA source de vérité du profil (bio, ville, emploi, entreprise,
5 −// site web, réseaux sociaux, photo, statut, profil public) — l'édition se
6 −// fait sur groupe-ka.com/compte, Vrai-Prix ne fait qu'afficher.
7 −// GET {hub}/api/sso/profile?client_id=vrai-prix&ka_id=…&ts=…&sig=…
8 −// avec sig = HMAC-SHA256(KA_SSO_SECRET, "vrai-prix.<ka_id>.<ts>") en hex
9 −// (même secret que le SSO). Cache mémoire 60 s (les 404 aussi — cache
10 −// négatif) ; null sur toute erreur (réseau, 401, 5xx, vieux compte non
11 −// relié) → l'appelant retombe sur les données locales de session.
12 −// Aucune dépendance : node:crypto + fetch natif seulement.
13 −// -----------------------------------------------------------------------------
14 −import { createHmac } from "node:crypto";
15 −import { CLIENT_ID, KA_HUB_URL } from "@/lib/ka-auth";
16 −
17 −const CACHE_TTL_MS = 60_000; // 60 s
18 −const TIMEOUT_MS = 5_000; // 5 s
19 −
20 −export type HubSocials = {
21 − instagram?: string;
22 − facebook?: string;
23 − x?: string;
24 − linkedin?: string;
25 − tiktok?: string;
26 − youtube?: string;
27 −};
28 −
29 −/** Réponse de GET /api/sso/profile du hub (200). */
30 −export type HubProfile = {
31 − ka_id: string;
32 − name: string;
33 − email: string;
34 − picture: string;
35 − role: string;
36 − role_label: string;
37 − bio: string;
38 − city: string;
39 − phone: string;
40 − website: string;
41 − job_title: string;
42 − company: string;
43 − birth_date: string;
44 − age: number | null;
45 − socials: HubSocials;
46 − public: boolean;
47 − public_url: string;
48 − created_at: string | number;
49 −};
50 −
51 −// Cache mémoire process-wide (Map globale — survit au HMR en dev) :
52 −// 200 → profil ; 404 → null (cache négatif). Erreurs transitoires : pas de cache.
53 −type CacheEntry = { at: number; data: HubProfile | null };
54 −const g = globalThis as unknown as { __vpHubProfileCache?: Map<string, CacheEntry> };
55 −const cache: Map<string, CacheEntry> =
56 − g.__vpHubProfileCache ?? (g.__vpHubProfileCache = new Map());
57 −
58 −/** Profil du membre au hub Groupe KA, ou null (inconnu ou injoignable). */
59 −export async function fetchHubProfile(kaId: string): Promise<HubProfile | null> {
60 − const secret = process.env.KA_SSO_SECRET;
61 − if (!secret || !kaId) return null;
62 −
63 − const now = Date.now();
64 − const hit = cache.get(kaId);
65 − if (hit && now - hit.at < CACHE_TTL_MS) return hit.data;
66 −
67 − const ts = Math.floor(now / 1000);
68 − const sig = createHmac("sha256", secret)
69 − .update(`${CLIENT_ID}.${kaId}.${ts}`)
70 − .digest("hex");
71 − const url =
72 − `${KA_HUB_URL}/api/sso/profile?client_id=${encodeURIComponent(CLIENT_ID)}` +
73 − `&ka_id=${encodeURIComponent(kaId)}&ts=${ts}&sig=${sig}`;
74 −
75 − const ctrl = new AbortController();
76 − const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
77 − try {
78 − const r = await fetch(url, { signal: ctrl.signal, cache: "no-store" });
79 − if (r.status === 404) {
80 − cache.set(kaId, { at: now, data: null }); // cache négatif 60 s
81 − return null;
82 − }
83 − if (!r.ok) return null; // transitoire (401, 5xx…) : pas de cache
84 − const data = (await r.json()) as unknown;
85 − if (typeof data !== "object" || data === null) return null;
86 − const profile = data as HubProfile;
87 − cache.set(kaId, { at: now, data: profile });
88 − return profile;
89 − } catch {
90 − return null; // réseau / timeout / JSON : pas de cache
91 − } finally {
92 − clearTimeout(timer);
93 − }
94 −}
deleted src/lib/hubfav.ts +0 −149
@@ -1,149 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// -----------------------------------------------------------------------------
3 −// Favoris « Mon univers Ka » — le hub Groupe KA (groupe-ka.com) est le MAGASIN
4 −// CENTRAL des favoris du groupe : Vrai-Prix ne stocke RIEN localement.
5 −// · Chaque ♥ est poussé au hub (POST signé HMAC, SYNCHRONE : un échec
6 −// remonte à l'appelant) ; la liste est lue au hub (GET signé, cache
7 −// mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.
8 −// · sig = HMAC-SHA256(KA_SSO_SECRET, "vrai-prix.<ka_id>.<ts>") en hex,
9 −// ts epoch en secondes (fenêtre ±300 s côté hub).
10 −// · Appels SERVEUR uniquement (route handlers) — jamais depuis le navigateur.
11 −// Aucune dépendance : node:crypto + fetch natif seulement.
12 −// Config .env.local : KA_SSO_SECRET, KA_HUB_URL (optionnel).
13 −// -----------------------------------------------------------------------------
14 −import { createHmac } from "node:crypto";
15 −import { CLIENT_ID, KA_HUB_URL } from "@/lib/ka-auth";
16 −
17 −const CACHE_TTL_MS = 30_000; // 30 s — liste des favoris
18 −const TIMEOUT_MS = 6_000; // 6 s
19 −
20 −/** Item de favori tel qu'accepté / rendu par le hub. */
21 −export type FavItem = {
22 − item_id: string;
23 − title: string;
24 − subtitle?: string;
25 − price_label?: string;
26 − image_url?: string;
27 − url?: string;
28 − meta?: Record<string, unknown>;
29 −};
30 −
31 −// champs d'item acceptés → longueur maximale (troncature défensive)
32 −const FIELDS: { key: Exclude<keyof FavItem, "meta">; max: number }[] = [
33 − { key: "item_id", max: 120 },
34 − { key: "title", max: 200 },
35 − { key: "subtitle", max: 200 },
36 − { key: "price_label", max: 60 },
37 − { key: "image_url", max: 500 },
38 − { key: "url", max: 500 },
39 −];
40 −
41 −/** Signature HMAC-SHA256 du hub : hex("vrai-prix.<ka_id>.<ts>"). */
42 −function sig(kaId: string, ts: number): string | null {
43 − const secret = process.env.KA_SSO_SECRET;
44 − if (!secret) return null;
45 − return createHmac("sha256", secret)
46 − .update(`${CLIENT_ID}.${kaId}.${ts}`)
47 − .digest("hex");
48 −}
49 −
50 −/** Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe). */
51 −export function linked(kaId: string | null | undefined): boolean {
52 − return !!kaId && String(kaId).startsWith("ka-");
53 −}
54 −
55 −/** Ne garde que les champs d'item connus (chaînes tronquées) — null si pas
56 − * d'item_id exploitable. `title` retombe sur l'item_id. */
57 −export function cleanItem(raw: unknown): FavItem | null {
58 − if (typeof raw !== "object" || raw === null) return null;
59 − const src = raw as Record<string, unknown>;
60 − const out: Record<string, unknown> = {};
61 − for (const { key, max } of FIELDS) {
62 − const v = src[key];
63 − if (v == null || v === "") continue;
64 − out[key] = String(v).slice(0, max);
65 − }
66 − if (typeof out.item_id !== "string" || !out.item_id) return null;
67 − if (typeof out.title !== "string" || !out.title) out.title = out.item_id;
68 − if (src.meta && typeof src.meta === "object" && !Array.isArray(src.meta)) {
69 − out.meta = src.meta as Record<string, unknown>;
70 − }
71 − return out as FavItem;
72 −}
73 −
74 −// Cache mémoire process-wide (Map globale — survit au HMR en dev).
75 −type CacheEntry = { at: number; favs: FavItem[] };
76 −const g = globalThis as unknown as { __vpHubFavCache?: Map<string, CacheEntry> };
77 −const cache: Map<string, CacheEntry> =
78 − g.__vpHubFavCache ?? (g.__vpHubFavCache = new Map());
79 −
80 −/** fetch avec délai maximal (AbortController) — null sur toute erreur réseau. */
81 −async function hubFetch(url: string, init?: RequestInit): Promise<Response | null> {
82 − const ctrl = new AbortController();
83 − const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
84 − try {
85 − return await fetch(url, { ...init, signal: ctrl.signal, cache: "no-store" });
86 − } catch {
87 − return null;
88 − } finally {
89 − clearTimeout(timer);
90 − }
91 −}
92 −
93 −/** Favoris Vrai-Prix du membre, lus au hub (cache mémoire 30 s).
94 − * [] = aucun favori ; null = hub injoignable (erreur, jamais mise en cache). */
95 −export async function hubList(kaId: string): Promise<FavItem[] | null> {
96 − if (!linked(kaId)) return []; // compte non relié au hub
97 − const now = Date.now();
98 − const hit = cache.get(kaId);
99 − if (hit && now - hit.at < CACHE_TTL_MS) return hit.favs;
100 −
101 − const ts = Math.floor(now / 1000);
102 − const s = sig(kaId, ts);
103 − if (!s) return null; // KA_SSO_SECRET absent
104 − const url =
105 − `${KA_HUB_URL}/api/sso/favorites?client_id=${encodeURIComponent(CLIENT_ID)}` +
106 − `&ka_id=${encodeURIComponent(kaId)}&ts=${ts}&sig=${s}`;
107 − const r = await hubFetch(url);
108 − if (!r || !r.ok) return null;
109 − let favs: unknown;
110 − try {
111 − favs = ((await r.json()) as { favorites?: unknown })?.favorites ?? [];
112 − } catch {
113 − return null;
114 − }
115 − if (!Array.isArray(favs)) return null;
116 − const clean = favs.filter(
117 − (f): f is FavItem =>
118 − typeof f === "object" && f !== null && typeof (f as FavItem).item_id === "string",
119 − );
120 − cache.set(kaId, { at: now, favs: clean });
121 − return clean;
122 −}
123 −
124 −/** Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :
125 − * le hub est le magasin des favoris, l'échec doit remonter à l'appelant. */
126 −export async function hubToggle(
127 − kaId: string,
128 − action: "add" | "remove",
129 − item: FavItem,
130 −): Promise<boolean> {
131 − const ts = Math.floor(Date.now() / 1000);
132 − const s = sig(kaId, ts);
133 − if (!s || !linked(kaId) || (action !== "add" && action !== "remove")) return false;
134 − const r = await hubFetch(`${KA_HUB_URL}/api/sso/favorites`, {
135 − method: "POST",
136 − headers: { "Content-Type": "application/json" },
137 − body: JSON.stringify({
138 − client_id: CLIENT_ID,
139 − ka_id: kaId,
140 − ts: String(ts),
141 − sig: s,
142 − action,
143 − item,
144 − }),
145 − });
146 − const ok = r?.status === 200;
147 − if (ok) cache.delete(kaId); // la prochaine lecture reflète le toggle
148 − return ok;
149 −}
modified src/lib/i18n.ts +99 −21
@@ -1,12 +1,12 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 export type Lang = "fr" | "en";
3 3
4 4 const dict = {
5 5 fr: {
6 − tagline: "La vraie valeur de votre propriété, en toute transparence",
7 − sub: "Estimation basée sur 3,7 millions d'unités d'évaluation et 745 000 ventes réelles au Québec (2021-2026). Pas de boîte noire : on vous montre le calcul.",
6 + tagline: "Comprendre comment une propriété est évaluée",
7 + sub: "Laboratoire pédagogique d'évaluation immobilière : 3,7 millions d'unités d'évaluation et 745 000 ventes réelles au Québec (2021-2026). Chaque estimation montre son calcul en entier — comparables, ajustements, pondérations, intervalle de confiance.",
8 8 searchPlaceholder: "Entrez une adresse (ex. 861 route Elgin, Saint-Pamphile)",
9 − estimateBtn: "Estimer",
9 + estimateBtn: "Analyser",
10 10 manualTitle: "Pas d'adresse ? Décrivez la propriété",
11 11 municipality: "Municipalité",
12 12 propertyType: "Type de propriété",
@@ -14,9 +14,9 @@ const dict = {
14 14 yearBuilt: "Année de construction",
15 15 landArea: "Superficie du terrain (m²)",
16 16 estimatedValue: "Valeur estimée",
17 − range: "Fourchette honnête",
17 + range: "Intervalle de confiance (P10-P90)",
18 18 confidence: "Indice de confiance",
19 − whyTitle: "Pourquoi ce prix ? Le calcul, sans boîte noire",
19 + whyTitle: "Comment cette valeur est calculée",
20 20 modelPart: "Modèle hédonique (LightGBM, 690 000 ventes QC)",
21 21 compsPart: "Comparables ajustés (médiane pondérée)",
22 22 weightModel: "poids du modèle",
@@ -31,11 +31,8 @@ const dict = {
31 31 weight: "Poids",
32 32 history: "Valeur estimée par année",
33 33 assessed: "Évaluation municipale (rôle 2026)",
34 − leadTitle: "Recevoir le rapport détaillé (PDF)",
35 − leadBtn: "M'envoyer le rapport",
36 − leadOk: "Merci ! Votre rapport sera envoyé sous peu.",
37 − emailPlaceholder: "votre@courriel.com",
38 − disclaimer: "Estimation statistique à titre indicatif — ne remplace pas une évaluation professionnelle.",
34 + disclaimer:
35 + "Plateforme à vocation pédagogique, universitaire et expérimentale — cette estimation statistique ne constitue pas une évaluation immobilière professionnelle certifiée (OEAQ).",
39 36 unifamilial: "Unifamiliale",
40 37 plex: "Plex (2-5 logements)",
41 38 condo_ou_multi: "Condo / multirésidentiel",
@@ -51,12 +48,54 @@ const dict = {
51 48 confD: "Peu fiable — marché mince",
52 49 searchNoResult: "Aucun résultat — essayez « numéro rue municipalité »",
53 50 loading: "Calcul en cours…",
51 + /* ---- couche pédagogique (mode étudiant) ---- */
52 + studentMode: "Mode étudiant",
53 + seeAnalysis: "Voir l'analyse",
54 + hideAnalysis: "Masquer l'analyse",
55 + pedIntro:
56 + "Le mode étudiant révèle les mécanismes du calcul : critères de sélection des comparables, formules d'ajustement, pondérations et construction de l'indice de confiance.",
57 + pedWhyCompsTitle: "Pourquoi ces comparables ?",
58 + pedWhyComps:
59 + "Les ventes candidates sont cherchées dans un rayon adaptatif (2,2 km, élargi si le marché est mince), puis filtrées : même famille de type de propriété, superficie habitable à ±20 % (relâchée à ±40 % si moins de 6 candidates). Les 12 meilleures selon leur poids sont retenues.",
60 + pedWeightTitle: "Comment le poids de chaque comparable est calculé",
61 + pedWeight:
62 + "Le poids est le produit de trois gaussiennes : proximité (σ = 1 500 m), récence (σ = 24 mois) et similarité de superficie (σ = 25 %). Une vente proche, récente et de taille semblable pèse donc beaucoup plus lourd dans la médiane pondérée.",
63 + pedAdjTitle: "Les trois ajustements, en dollars",
64 + pedAdjTime:
65 + "Ajustement de marché : le prix de vente est ramené aux conditions actuelles via l'indice mensuel du type de propriété (une vente de 2023 est « revalorisée » au niveau du marché d'aujourd'hui).",
66 + pedAdjArea:
67 + "Ajustement de superficie : 50 % du prix au m² du comparable par m² d'écart avec la propriété analysée, plafonné à ±25 % du prix.",
68 + pedAdjAge:
69 + "Ajustement d'âge : 0,5 % du prix par année d'écart de construction, plafonné à ±10 %.",
70 + pedBlendTitle: "La combinaison finale",
71 + pedBlend:
72 + "Lorsque les deux sources existent, l'estimation finale = 65 % modèle hédonique + 35 % comparables ajustés. Le modèle capte les tendances de masse (18 variables) ; les comparables ancrent l'estimation dans le marché local récent.",
73 + pedConfTitle: "Décomposition de l'indice de confiance",
74 + pedConfBase: "Base",
75 + pedConfComps: "Nombre de comparables (+2,5 chacun, max +25)",
76 + pedConfDisp: "Faible dispersion des comparables (max +20)",
77 + pedConfModel: "Présence du modèle hédonique (+15)",
78 + pedConfWidth: "Pénalité de largeur de fourchette (au-delà de 30 %)",
79 + pedConfTotal: "Score final",
80 + pedConceptsTitle: "Trois notions à ne pas confondre",
81 + pedConceptMarket: "Valeur marchande",
82 + pedConceptMarketDef:
83 + "Le prix le plus probable sur un marché libre et concurrentiel — c'est ce que la plateforme estime.",
84 + pedConceptAsking: "Prix demandé",
85 + pedConceptAskingDef:
86 + "Le prix affiché par le vendeur — une stratégie de mise en marché, souvent au-dessus (ou parfois sous) la valeur marchande.",
87 + pedConceptSale: "Prix de transaction",
88 + pedConceptSaleDef:
89 + "Le prix réellement payé, observé dans les 745 000 ventes du corpus — la seule mesure directe du marché.",
90 + pedRoleTitle: "Et la valeur au rôle ?",
91 + pedRole:
92 + "L'évaluation municipale sert au calcul de la taxe foncière. Elle est établie 18 à 24 mois avant son entrée en vigueur : dans un marché qui monte, elle est souvent sous la valeur marchande courante.",
54 93 },
55 94 en: {
56 − tagline: "Your property's true value, fully transparent",
57 − sub: "Estimates built on 3.7M assessment units and 745k real Québec sales (2021-2026). No black box: we show the math.",
95 + tagline: "Understand how a property is appraised",
96 + sub: "A teaching laboratory for real-estate valuation: 3.7M assessment units and 745k real Québec sales (2021-2026). Every estimate shows its full computation — comparables, adjustments, weights, confidence interval.",
58 97 searchPlaceholder: "Enter an address (e.g. 861 route Elgin, Saint-Pamphile)",
59 − estimateBtn: "Estimate",
98 + estimateBtn: "Analyze",
60 99 manualTitle: "No address? Describe the property",
61 100 municipality: "Municipality",
62 101 propertyType: "Property type",
@@ -64,9 +103,9 @@ const dict = {
64 103 yearBuilt: "Year built",
65 104 landArea: "Lot area (m²)",
66 105 estimatedValue: "Estimated value",
67 − range: "Honest range",
106 + range: "Confidence interval (P10-P90)",
68 107 confidence: "Confidence index",
69 − whyTitle: "Why this price? The math, no black box",
108 + whyTitle: "How this value is computed",
70 109 modelPart: "Hedonic model (LightGBM, 690k QC sales)",
71 110 compsPart: "Adjusted comparables (weighted median)",
72 111 weightModel: "model weight",
@@ -81,11 +120,8 @@ const dict = {
81 120 weight: "Weight",
82 121 history: "Estimated value by year",
83 122 assessed: "Municipal assessment (2026 roll)",
84 − leadTitle: "Get the detailed report (PDF)",
85 − leadBtn: "Send me the report",
86 − leadOk: "Thanks! Your report is on its way.",
87 − emailPlaceholder: "you@email.com",
88 − disclaimer: "Statistical estimate for information only — not a substitute for a professional appraisal.",
123 + disclaimer:
124 + "Educational, academic and experimental platform — this statistical estimate is not a certified professional appraisal (OEAQ).",
89 125 unifamilial: "Single-family",
90 126 plex: "Plex (2-5 units)",
91 127 condo_ou_multi: "Condo / multi-residential",
@@ -101,6 +137,48 @@ const dict = {
101 137 confD: "Low reliability — thin market",
102 138 searchNoResult: "No result — try “number street municipality”",
103 139 loading: "Computing…",
140 + /* ---- pedagogical layer (student mode) ---- */
141 + studentMode: "Student mode",
142 + seeAnalysis: "See the analysis",
143 + hideAnalysis: "Hide the analysis",
144 + pedIntro:
145 + "Student mode reveals the mechanics of the computation: comparable selection criteria, adjustment formulas, weights, and how the confidence index is built.",
146 + pedWhyCompsTitle: "Why these comparables?",
147 + pedWhyComps:
148 + "Candidate sales are fetched in an adaptive radius (2.2 km, widened in thin markets), then filtered: same property-type family, living area within ±20% (relaxed to ±40% below 6 candidates). The best 12 by weight are kept.",
149 + pedWeightTitle: "How each comparable's weight is computed",
150 + pedWeight:
151 + "The weight is the product of three Gaussians: proximity (σ = 1,500 m), recency (σ = 24 months) and area similarity (σ = 25%). A nearby, recent, similarly sized sale therefore weighs much more in the weighted median.",
152 + pedAdjTitle: "The three adjustments, in dollars",
153 + pedAdjTime:
154 + "Market adjustment: the sale price is brought to current conditions via the property type's monthly index (a 2023 sale is 're-priced' at today's market level).",
155 + pedAdjArea:
156 + "Area adjustment: 50% of the comparable's $/m² per m² of difference with the subject, capped at ±25% of the price.",
157 + pedAdjAge:
158 + "Age adjustment: 0.5% of the price per year of construction difference, capped at ±10%.",
159 + pedBlendTitle: "The final blend",
160 + pedBlend:
161 + "When both sources exist, the final estimate = 65% hedonic model + 35% adjusted comparables. The model captures mass trends (18 variables); the comparables anchor the estimate in the recent local market.",
162 + pedConfTitle: "Confidence index breakdown",
163 + pedConfBase: "Base",
164 + pedConfComps: "Number of comparables (+2.5 each, max +25)",
165 + pedConfDisp: "Low comparable dispersion (max +20)",
166 + pedConfModel: "Hedonic model available (+15)",
167 + pedConfWidth: "Range-width penalty (beyond 30%)",
168 + pedConfTotal: "Final score",
169 + pedConceptsTitle: "Three notions not to confuse",
170 + pedConceptMarket: "Market value",
171 + pedConceptMarketDef:
172 + "The most probable price on an open, competitive market — this is what the platform estimates.",
173 + pedConceptAsking: "Asking price",
174 + pedConceptAskingDef:
175 + "The price posted by the seller — a marketing strategy, often above (sometimes below) market value.",
176 + pedConceptSale: "Transaction price",
177 + pedConceptSaleDef:
178 + "The price actually paid, observed in the 745k sales of the corpus — the only direct measure of the market.",
179 + pedRoleTitle: "What about the assessed value?",
180 + pedRole:
181 + "The municipal assessment is used to compute property tax. It is set 18-24 months before taking effect: in a rising market, it often sits below current market value.",
104 182 },
105 183 } as const;
106 184
deleted src/lib/ka-auth.ts +0 −179
@@ -1,179 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// -----------------------------------------------------------------------------
3 −// KA ID — SSO du Groupe KA (hub d'identité : groupe-ka.com).
4 −// « Se connecter avec KA ID » : l'utilisateur est envoyé au hub, qui le
5 −// renvoie ici avec un jeton JWT HS256 signé du secret partagé KA_SSO_SECRET
6 −// et transportant son profil (dont le KA-ID « ka-0123456789 », LE MÊME sur
7 −// toutes les plateformes du groupe). Aucune dépendance : node:crypto seulement.
8 −// · state / session locale : base64url(JSON) + "." + HMAC-SHA256 hex,
9 −// signés avec KA_AUTH_SECRET (jamais partagé).
10 −// · ka_token du hub : JWT HS256 vérifié avec KA_SSO_SECRET
11 −// (signature, alg, iss, aud, exp).
12 −// -----------------------------------------------------------------------------
13 −import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
14 −
15 −export const SESSION_COOKIE = "vp_ka_session";
16 −export const SESSION_DAYS = 30;
17 −export const CLIENT_ID = "vrai-prix";
18 −
19 −export const KA_HUB_URL = (
20 − process.env.KA_HUB_URL || "https://www.groupe-ka.com"
21 −).replace(/\/+$/, "");
22 −
23 −/** URL publique de l'app (redirect_uri — le hub n'accepte que vrai-prix.com). */
24 −export const KA_BASE_URL = (
25 − process.env.KA_BASE_URL || "https://www.vrai-prix.com"
26 −).replace(/\/+$/, "");
27 −
28 −/** Session locale posée en cookie httpOnly après un retour SSO réussi. */
29 −export type KaSession = {
30 − ka_id: string;
31 − email: string;
32 − name: string;
33 − picture: string;
34 − provider?: string;
35 − exp: number; // epoch (secondes)
36 −};
37 −
38 −const now = () => Math.floor(Date.now() / 1000);
39 −
40 −function authSecret(): Buffer | null {
41 − const s = process.env.KA_AUTH_SECRET;
42 − return s ? Buffer.from(s, "utf8") : null;
43 −}
44 −
45 −function ssoSecret(): Buffer | null {
46 − const s = process.env.KA_SSO_SECRET;
47 − return s ? Buffer.from(s, "utf8") : null;
48 −}
49 −
50 −/** Comparaison à temps constant (timingSafeEqual exige des longueurs égales). */
51 −function safeEqual(a: Buffer, b: Buffer): boolean {
52 − return a.length === b.length && timingSafeEqual(a, b);
53 −}
54 −
55 −// -- jeton local signé (state SSO + session) : base64url(JSON).hex(HMAC) ------
56 −
57 −export function signLocal(payload: Record<string, unknown>): string | null {
58 − const secret = authSecret();
59 − if (!secret) return null;
60 − const raw = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
61 − const sig = createHmac("sha256", secret).update(raw).digest("hex");
62 − return `${raw}.${sig}`;
63 −}
64 −
65 −export function verifyLocal(token: string): Record<string, unknown> | null {
66 − const secret = authSecret();
67 − if (!secret || !token) return null;
68 − try {
69 − const dot = token.lastIndexOf(".");
70 − if (dot < 0) return null;
71 − const raw = token.slice(0, dot);
72 − const sig = token.slice(dot + 1);
73 − const expected = createHmac("sha256", secret).update(raw).digest("hex");
74 − if (!safeEqual(Buffer.from(sig, "utf8"), Buffer.from(expected, "utf8"))) {
75 − return null;
76 − }
77 − const payload = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
78 − if (typeof payload !== "object" || payload === null) return null;
79 − if (typeof payload.exp !== "number" || payload.exp < now()) return null;
80 − return payload as Record<string, unknown>;
81 − } catch {
82 − return null;
83 − }
84 −}
85 −
86 −// -- state SSO (anti-forgerie, transporte aussi la destination `next`) --------
87 −
88 −/** State opaque signé localement — expire en 10 minutes. */
89 −export function newState(next: string): string | null {
90 − return signLocal({
91 − n: randomBytes(12).toString("base64url"),
92 − next,
93 − exp: now() + 600,
94 − });
95 −}
96 −
97 −/** Vérifie le state et rend la destination `next` (chemin local sûr). */
98 −export function verifyState(state: string): { next: string } | null {
99 − const p = verifyLocal(state);
100 − if (!p) return null;
101 − const next = typeof p.next === "string" ? p.next : "/compte";
102 − return { next: sanitizeNext(next) };
103 −}
104 −
105 −/** N'accepte qu'un chemin local (« /… », jamais « //… » ni une URL absolue). */
106 −export function sanitizeNext(next: string | null | undefined): string {
107 − if (!next || !next.startsWith("/") || next.startsWith("//")) return "/compte";
108 − return next;
109 −}
110 −
111 −// -- ka_token : JWT HS256 émis par le hub (vérification stdlib) ---------------
112 −
113 −export function verifyKaToken(token: string): Record<string, unknown> | null {
114 − const secret = ssoSecret();
115 − if (!secret || !token) return null;
116 − try {
117 − const parts = token.split(".");
118 − if (parts.length !== 3) return null;
119 − const [h64, p64, s64] = parts;
120 − const expected = createHmac("sha256", secret)
121 − .update(`${h64}.${p64}`)
122 − .digest();
123 − if (!safeEqual(Buffer.from(s64, "base64url"), expected)) return null;
124 −
125 − const header = JSON.parse(Buffer.from(h64, "base64url").toString("utf8"));
126 − if (header?.alg !== "HS256") return null;
127 −
128 − const claims = JSON.parse(Buffer.from(p64, "base64url").toString("utf8"));
129 − if (claims?.iss !== KA_HUB_URL) return null;
130 − const aud = claims?.aud;
131 − const audOk =
132 − aud === CLIENT_ID || (Array.isArray(aud) && aud.includes(CLIENT_ID));
133 − if (!audOk) return null;
134 − if (typeof claims?.exp !== "number" || claims.exp < now()) return null;
135 − return claims as Record<string, unknown>;
136 − } catch {
137 − return null;
138 − }
139 −}
140 −
141 −// -- session locale ------------------------------------------------------------
142 −
143 −/** Construit le cookie de session (30 jours) depuis les claims du hub. */
144 −export function sessionFromClaims(
145 − claims: Record<string, unknown>,
146 −): { value: string; session: KaSession } | null {
147 − const kaId = typeof claims.ka_id === "string" ? claims.ka_id.trim() : "";
148 − if (!kaId) return null; // le KA-ID est créé À LA SOURCE par le hub — requis
149 − const email =
150 − typeof claims.email === "string" ? claims.email.toLowerCase() : "";
151 − const session: KaSession = {
152 − ka_id: kaId,
153 − email,
154 − name:
155 − typeof claims.name === "string" && claims.name
156 − ? claims.name
157 − : email.split("@")[0] || "membre",
158 − picture: typeof claims.picture === "string" ? claims.picture : "",
159 − provider: typeof claims.provider === "string" ? claims.provider : undefined,
160 − exp: now() + SESSION_DAYS * 86400,
161 − };
162 − const value = signLocal({ ...session });
163 − return value ? { value, session } : null;
164 −}
165 −
166 −/** Lit et vérifie le cookie de session — null si absent/invalide/expiré. */
167 −export function readSession(cookieValue: string | undefined): KaSession | null {
168 − if (!cookieValue) return null;
169 − const p = verifyLocal(cookieValue);
170 − if (!p || typeof p.ka_id !== "string" || !p.ka_id) return null;
171 − return {
172 − ka_id: p.ka_id,
173 − email: typeof p.email === "string" ? p.email : "",
174 − name: typeof p.name === "string" ? p.name : "",
175 − picture: typeof p.picture === "string" ? p.picture : "",
176 − provider: typeof p.provider === "string" ? p.provider : undefined,
177 − exp: p.exp as number,
178 − };
179 −}
modified src/lib/ka/tools.ts +5 −5
@@ -1,6 +1,6 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Ka — la panoplie d'outils de l'agent IA de Vrai-Prix.
3 + * Éva — la panoplie d'outils de l'assistante pédagogique d'UQO Éval.
4 4 * Chaque outil est exécuté côté serveur, directement sur la base SQLite
5 5 * (3,75 M unités) et le moteur d'estimation. Les sorties sont des JSON
6 6 * compacts : Ka lit vite, coûte peu, et cite des chiffres exacts.
@@ -93,9 +93,9 @@ const ID_ERR =
93 93 function liens(id: string) {
94 94 const e = encodeURIComponent(id);
95 95 return {
96 − fiche_complete: `https://www.vrai-prix.com/estimation/${e}`,
97 − rapport_standard_pdf: `https://www.vrai-prix.com/api/report?id=${e}`,
98 − rapport_professionnel_pdf: `https://www.vrai-prix.com/api/report/pro?id=${e}`,
96 + fiche_complete: `https://www.uqo-eval.app/estimation/${e}`,
97 + rapport_standard_pdf: `https://www.uqo-eval.app/api/report?id=${e}`,
98 + rapport_professionnel_pdf: `https://www.uqo-eval.app/api/report/pro?id=${e}`,
99 99 };
100 100 }
101 101
deleted src/lib/kaid.ts +0 −124
@@ -1,124 +0,0 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// -----------------------------------------------------------------------------
3 −// Groupe KA — kaid.ts : client KA ID v2 (personnalisation) pour les apps Next.
4 −// SOURCE CANONIQUE : ka-ui.git/kaid/kaid.ts — copié dans src/lib/kaid.ts des
5 −// apps TypeScript (vrai-prix, valoplex, trouve-ka). Ne pas diverger.
6 −//
7 −// Équivalent TS de kaid.py : track() (journal, fire-and-forget) et
8 −// fetchPrefs() (profil appris, cache 90 s, fail-open). Appels SERVEUR
9 −// uniquement (route handlers) — le secret SSO ne va jamais au navigateur.
10 −// sig = HMAC-SHA256(KA_SSO_SECRET, `${clientId}.${kaId}.${ts}`) en hex.
11 −// Aucune dépendance : node:crypto + fetch natif.
12 −// -----------------------------------------------------------------------------
13 −import { createHmac } from "node:crypto";
14 −
15 −export type KaidEvent = {
16 − type: string;
17 − entity_type?: string;
18 − entity_id?: string;
19 − query?: string;
20 − filters?: Record<string, unknown>;
21 − position?: number;
22 − features?: Record<string, unknown>;
23 − dwell_ms?: number;
24 − session_id?: string;
25 −};
26 −
27 −export type KaidPrefs = {
28 − ok: boolean;
29 − personalization: boolean;
30 − profile: {
31 − app: {
32 − n: number;
33 − dims: Record<string, { values: Record<string, number>; conf: number }>;
34 − ranges: Record<string, { p25: number; p50: number; p75: number; n: number }>;
35 − } | null;
36 − global: { location: { values: Record<string, number>; conf: number } };
37 − } | null;
38 − hidden: string[];
39 −} | null;
40 −
41 −const TIMEOUT_MS = 5_000;
42 −const PREFS_TTL_MS = 90_000;
43 −const prefsCache = new Map<string, { t: number; data: KaidPrefs }>();
44 −
45 −function hubUrl(): string {
46 − return (process.env.KA_HUB_URL ?? "https://www.groupe-ka.com").replace(/\/+$/, "");
47 −}
48 −
49 −function signedParams(clientId: string, kaId: string): URLSearchParams | null {
50 − const secret = process.env.KA_SSO_SECRET;
51 − if (!secret || !kaId.startsWith("ka-")) return null;
52 − const ts = String(Math.floor(Date.now() / 1000));
53 − const sig = createHmac("sha256", secret)
54 − .update(`${clientId}.${kaId}.${ts}`)
55 − .digest("hex");
56 − return new URLSearchParams({ client_id: clientId, ka_id: kaId, ts, sig });
57 −}
58 −
59 −/** Journalise des événements au hub — fire-and-forget, jamais bloquant. */
60 −export function track(
61 − clientId: string,
62 − kaId: string | null | undefined,
63 − events: KaidEvent[],
64 −): void {
65 − if (!kaId || !events.length) return;
66 − const p = signedParams(clientId, kaId);
67 − if (!p) return;
68 − const body = JSON.stringify({
69 − ...Object.fromEntries(p),
70 − events: events.slice(0, 20),
71 − });
72 − fetch(`${hubUrl()}/api/sso/events`, {
73 − method: "POST",
74 − headers: { "Content-Type": "application/json" },
75 − body,
76 − signal: AbortSignal.timeout(TIMEOUT_MS),
77 − }).catch(() => {});
78 −}
79 −
80 −/** Profil de personnalisation (cache 90 s) — null si indisponible (fail-open). */
81 −export async function fetchPrefs(
82 − clientId: string,
83 − kaId: string | null | undefined,
84 −): Promise<KaidPrefs> {
85 − if (!kaId) return null;
86 − const hit = prefsCache.get(kaId);
87 − if (hit && Date.now() - hit.t < PREFS_TTL_MS) return hit.data;
88 − const p = signedParams(clientId, kaId);
89 − if (!p) return null;
90 − let data: KaidPrefs = null;
91 − try {
92 − const r = await fetch(`${hubUrl()}/api/sso/prefs?${p}`, {
93 − signal: AbortSignal.timeout(TIMEOUT_MS),
94 − });
95 − if (r.ok) data = (await r.json()) as KaidPrefs;
96 − } catch {
97 − data = null;
98 − }
99 − prefsCache.set(kaId, { t: Date.now(), data });
100 − if (prefsCache.size > 500)
101 − for (const k of [...prefsCache.keys()].slice(0, 100)) prefsCache.delete(k);
102 − return data;
103 −}
104 −
105 −/** Affinité [−1,1] d'une valeur dans une dimension du profil (0 si inconnue). */
106 −export function affinity(
107 − prefs: KaidPrefs,
108 − dim: string,
109 − value: string | null | undefined,
110 −): number {
111 − const d = prefs?.profile?.app?.dims?.[dim];
112 − if (!d || !value) return 0;
113 − return d.values[value.trim().toLowerCase()] ?? 0;
114 −}
115 −
116 −/** Affinité de localisation transversale (tous univers confondus). */
117 −export function globalLocationAffinity(
118 − prefs: KaidPrefs,
119 − value: string | null | undefined,
120 −): number {
121 − const loc = prefs?.profile?.global?.location;
122 − if (!loc || !value) return 0;
123 − return (loc.values[value.trim().toLowerCase()] ?? 0) * (loc.conf || 0.3);
124 −}
modified src/lib/kamaps.ts +24 −20
@@ -1,21 +1,25 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −// Projet : Groupe Ka / Ka Maps (intégration Vrai-Prix)
3 −// lib/kamaps.ts : thème Vrai-Prix Maps + adaptateur /api/nearby.
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 +// lib/kamaps.ts : thème UQO Éval Maps (framework Ka Maps) + adaptateur /api/nearby.
4 3 // La carte affiche des VALEURS ESTIMÉES (jamais des prix demandés) :
5 −// pastilles encre à liseré rouge #ff5148, sélection rouge — le langage
6 −// visuel du site (encre #141814, « lime » rouge, bordeaux #9e2a25).
4 +// pastilles encre à liseré turquoise UQO, sélection bleu UQO — le langage
5 +// visuel du site (encre #16181b, bleu #006085, turquoise #2bccae).
7 6
8 7 import type {
9 8 BoundsQuery,
10 9 BoundsQueryResult,
10 + KaAppSource,
11 11 KaDataAdapter,
12 12 KaMapTheme,
13 13 MapProperty,
14 14 } from "@groupe-ka/ka-maps";
15 15
16 −const INK = "#141814";
17 −const RED = "#ff5148"; // --lime (nom hérité)
18 −const BORDEAUX = "#9e2a25"; // --green (nom hérité)
16 +// Le framework ka-maps fige l'union KaAppSource à ses apps d'origine ;
17 +// « uqo-eval » n'y figure pas — l'id ne sert qu'au thème/URL, déjà fournis ici.
18 +const APP_SOURCE = "uqo-eval" as KaAppSource;
19 +
20 +const INK = "#16181b";
21 +const RED = "#2bccae"; // turquoise UQO (liseré/halo)
22 +const BORDEAUX = "#00465f"; // bleu UQO profond (grappes)
19 23
20 24 export const MAPBOX_TOKEN: string =
21 25 process.env.NEXT_PUBLIC_MAPBOX_TOKEN ??
@@ -34,10 +38,10 @@ export const TYPE_FR: Record<string, string> = {
34 38 autre: "Autre",
35 39 };
36 40
37 −export const vraiPrixMapTheme: KaMapTheme = {
38 − id: "vrai-prix",
39 − productName: "Vrai-Prix Maps",
40 − accent: RED,
41 +export const uqoEvalMapTheme: KaMapTheme = {
42 + id: "uqo-eval",
43 + productName: "UQO Éval Maps",
44 + accent: "#006085", // bleu UQO
41 45 onAccent: "#ffffff",
42 46 fontFamily: "var(--font-inter), system-ui, sans-serif",
43 47 markers: {
@@ -45,23 +49,23 @@ export const vraiPrixMapTheme: KaMapTheme = {
45 49 background: INK,
46 50 text: "#ffffff",
47 51 halo: RED,
48 − selectedBackground: RED,
52 + selectedBackground: "#006085",
49 53 selectedText: "#ffffff",
50 54 },
51 − // Valeurs estimées : pastille encre, liseré rouge — visuellement
55 + // Valeurs estimées : pastille encre, liseré turquoise — visuellement
52 56 // distinctes d'un prix demandé (jamais présentées comme une annonce).
53 57 valuation: {
54 58 background: INK,
55 59 text: "#ffffff",
56 60 halo: RED,
57 − selectedBackground: RED,
61 + selectedBackground: "#006085",
58 62 selectedText: "#ffffff",
59 63 },
60 64 },
61 65 cluster: {
62 66 background: BORDEAUX,
63 67 text: "#ffffff",
64 − border: "rgba(255,81,72,0.5)",
68 + border: "rgba(43,204,174,0.5)",
65 69 },
66 70 supportsDark: false,
67 71 };
@@ -80,7 +84,7 @@ interface NearbyUnit {
80 84 function toMapProperty(u: NearbyUnit): MapProperty {
81 85 return {
82 86 id: u.id,
83 − appSource: "vrai-prix",
87 + appSource: APP_SOURCE,
84 88 latitude: u.lat,
85 89 longitude: u.lng,
86 90 kind: "valuation",
@@ -94,9 +98,9 @@ function toMapProperty(u: NearbyUnit): MapProperty {
94 98 }
95 99
96 100 /** Adaptateur viewport→données : /api/nearby (contrat partagé avec iOS). */
97 −export const vraiPrixMapAdapter: KaDataAdapter = {
98 − id: "vrai-prix-nearby",
99 − appSource: "vrai-prix",
101 +export const uqoEvalMapAdapter: KaDataAdapter = {
102 + id: "uqo-eval-nearby",
103 + appSource: APP_SOURCE,
100 104 async fetchInBounds(query: BoundsQuery): Promise<BoundsQueryResult> {
101 105 if (query.zoom < MIN_ZOOM_FETCH) return { properties: [] };
102 106 const { bbox } = query;
modified src/lib/municipalites.ts +1 −1
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Données agrégées par municipalité pour les pages programmatiques SEO.
3 3 // Les agrégats ne changent qu'au rebuild de la DB : cache mémoire par processus.
4 4
modified src/lib/report-portfolio.ts +26 −22
@@ -1,19 +1,20 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * Rapport PDF de parc immobilier — couverture agrégée (tuiles, historique du parc,
4 4 * répartitions, table des propriétés) puis une page condensée par propriété.
5 − * 100 % vectoriel, identité Vrai-Prix.
5 + * 100 % vectoriel, identité UQO Éval (logo officiel UQO).
6 + * Généré dans le cadre des cours IMM1003 et IMM1033 (UQO).
6 7 */
7 8 import PDFDocument from "pdfkit";
8 9 import path from "path";
9 10 import type { PortfolioResult, UnitEstimate } from "./estimator";
10 11
11 −const INK = "#141814";
12 +const INK = "#16181b";
12 13 const INK3 = "#8b928c";
13 −const PAPER = "#f5f3ee";
14 −const SURFACE2 = "#faf9f5";
15 −const GREEN = "#9e2a25";
16 −const LIME = "#ff5148";
14 +const PAPER = "#f4f5f3";
15 +const SURFACE2 = "#f9fafa";
16 +const GREEN = "#005F85"; // bleu UQO (PMS 7469)
17 +const LIME = "#2BCCAE"; // turquoise UQO
17 18 const LIME_SOFT = "#ffe3e0";
18 19 const AMBER_SOFT = "#fdf3e2";
19 20 const DANGER_SOFT = "#fbe9e7";
@@ -36,19 +37,17 @@ const TYPE_FR: Record<string, string> = {
36 37
37 38 type Doc = InstanceType<typeof PDFDocument>;
38 39
40 +const UQO_LOGO_BLANC_PATH = pathJoinUqo("UQO_blanc.png");
41 +function pathJoinUqo(f: string) {
42 + return path.join(process.cwd(), "assets", "uqo", f);
43 +}
44 +/** Logo officiel UQO (blanc, bandeau encre) + wordmark « Éval » — proportions natives. */
39 45 function logo(doc: Doc, x: number, y: number, scale = 1) {
40 − doc.save();
41 − doc.font("SG-Bold").fontSize(24 * scale);
42 − doc.fillColor(PAPER).text("Vrai", x, y, { lineBreak: false });
43 − const w = doc.widthOfString("Vrai");
44 − const bx = x + w + 4 * scale;
45 − doc.save();
46 − doc.rotate(-3, { origin: [bx, y + 12 * scale] });
47 − const bw = doc.widthOfString("Prix") + 12 * scale;
48 − doc.roundedRect(bx, y - 3 * scale, bw, 30 * scale, 5 * scale).fill(LIME);
49 − doc.fillColor(INK).text("Prix", bx + 6 * scale, y, { lineBreak: false });
50 − doc.restore();
51 − doc.restore();
46 + const h = 26 * scale;
47 + const w = h * (2123 / 853);
48 + doc.image(UQO_LOGO_BLANC_PATH, x, y, { height: h });
49 + doc.font("SG-Bold").fontSize(22 * scale).fillColor(LIME)
50 + .text("Éval", x + w + 8 * scale, y + h - 22 * scale, { lineBreak: false });
52 51 }
53 52
54 53 function kicker(doc: Doc, txt: string, x: number, y: number) {
@@ -61,8 +60,11 @@ function pageFrame(doc: Doc, page: number, total: number, generated: string) {
61 60 doc.rect(0, 0, W, H).fill(PAPER);
62 61 doc.rect(M, H - 46, W - 2 * M, 1.2).fill(INK);
63 62 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3).text(
64 − "VRAI-PRIX.COM — RAPPORT DE PARC IMMOBILIER · ESTIMATION STATISTIQUE À TITRE INDICATIF · SIMON-PIERRE BOUCHER · CONTACT@SPBOUCHER.AI",
63 + "UQO-EVAL.APP — RAPPORT DE PARC IMMOBILIER · PLATEFORME PÉDAGOGIQUE · ESTIMATION STATISTIQUE À TITRE INDICATIF",
65 64 M, H - 38, { width: W - 2 * M - 60, characterSpacing: 0.4, lineBreak: false });
65 + doc.font("JB-Reg").fontSize(6.5).fillColor(INK3).text(
66 + "COURS IMM1003 · IMM1033 — SIMON-PIERRE BOUCHER, UQO",
67 + M, H - 29, { width: W - 2 * M - 60, characterSpacing: 0.4, lineBreak: false });
66 68 doc.font("JB-Bold").fontSize(7.5).fillColor(INK)
67 69 .text(`${page} / ${total}`, W - M - 40, H - 39, { width: 40, align: "right" });
68 70 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)
@@ -194,7 +196,7 @@ export async function buildPortfolioReport(pf: PortfolioResult): Promise<Buffer>
194 196 const generated = new Date().toISOString().slice(0, 16).replace("T", " ");
195 197 const pageTotal = 1 + items.length;
196 198
197 − const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: "Vrai-Prix — Rapport de parc immobilier" } });
199 + const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: "UQO Éval — Rapport de parc immobilier", Author: "UQO Éval — cours IMM1003 · IMM1033 (Simon-Pierre Boucher, UQO)" } });
198 200 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));
199 201 doc.registerFont("SG-Med", F("SpaceGrotesk-Medium.ttf"));
200 202 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));
@@ -210,7 +212,9 @@ export async function buildPortfolioReport(pf: PortfolioResult): Promise<Buffer>
210 212 doc.rect(0, 0, W, 110).fill(INK);
211 213 logo(doc, M, 32);
212 214 doc.font("JB-Reg").fontSize(7).fillColor(LIME)
213 − .text("RAPPORT DE PARC IMMOBILIER · QUÉBEC", M, 70, { characterSpacing: 1.6, lineBreak: false });
215 + .text("RAPPORT DE PARC IMMOBILIER · LABORATOIRE PÉDAGOGIQUE UQO ÉVAL", M, 70, { characterSpacing: 1.2, lineBreak: false });
216 + doc.font("JB-Reg").fontSize(6).fillColor("#8fb8c9")
217 + .text("COURS IMM1003 · IMM1033 — SIMON-PIERRE BOUCHER, UQO", M, 82, { characterSpacing: 1, lineBreak: false });
214 218 doc.font("JB-Reg").fontSize(7).fillColor("#9aa39c")
215 219 .text(`${a.count} PROPRIÉTÉS`, W - M - 200, 38, { width: 200, align: "right" })
216 220 .text(`GÉNÉRÉ LE ${generated}`, W - M - 200, 50, { width: 200, align: "right" });
modified src/lib/report-pro.ts +45 −33
@@ -1,4 +1,4 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 3 * Rapport d'évaluation détaillé — format professionnel (présentation bancaire).
4 4 * Structure calquée sur un rapport d'évaluation : page titre, sommaire exécutif,
@@ -6,18 +6,19 @@
6 6 * de la propriété, grille d'ajustement des comparables, analyse hédonique,
7 7 * réconciliation, hypothèses et conditions limitatives, certification.
8 8 * Ton sobre, encadrés honnêtes : AVM statistique — n'est PAS un rapport OEAQ.
9 + * Généré dans le cadre des cours IMM1003 et IMM1033 (UQO), avec logo officiel UQO.
9 10 */
10 11 import PDFDocument from "pdfkit";
11 12 import path from "path";
12 13 import type { UnitEstimate } from "./estimator";
13 14
14 −const INK = "#141814";
15 +const INK = "#16181b";
15 16 const INK2 = "#4d5551";
16 17 const INK3 = "#8b928c";
17 −const SURFACE2 = "#faf9f5";
18 −const GREEN = "#9e2a25";
19 −const GREEN_DEEP = "#771f1b";
20 −const LIME = "#ff5148";
18 +const SURFACE2 = "#f9fafa";
19 +const GREEN = "#005F85"; // bleu UQO (PMS 7469)
20 +const GREEN_DEEP = "#00465F";
21 +const LIME = "#2BCCAE"; // turquoise UQO
21 22
22 23 const W = 612;
23 24 const H = 792;
@@ -25,6 +26,13 @@ const M = 56;
25 26 const CW = W - 2 * M;
26 27
27 28 const F = (f: string) => path.join(process.cwd(), "assets", "fonts", f);
29 +const UQO_LOGO_BLEU = path.join(process.cwd(), "assets", "uqo", "UQO_couleur.png");
30 +const UQO_RATIO = 2123 / 853; // ratio natif du logo — jamais déformé
31 +
32 +/** Mention pédagogique obligatoire sur tous les rapports UQO Éval. */
33 +const COURS =
34 + "Rapport généré dans le cadre des cours IMM1003 — Éléments d'évaluation immobilière et " +
35 + "IMM1033 — Méthodes du coût en évaluation immobilière, enseignés par Simon-Pierre Boucher (UQO).";
28 36 const fmt = (v: number | null | undefined) =>
29 37 v == null
30 38 ? "—"
@@ -45,12 +53,11 @@ interface Ctx {
45 53
46 54 function header(c: Ctx) {
47 55 const { doc } = c;
48 − doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Vrai", M, 26, { lineBreak: false });
49 − const w = doc.widthOfString("Vrai");
50 − doc.rect(M + w + 2, 25, doc.widthOfString("Prix") + 6, 13).fill(INK);
51 − doc.fillColor(LIME).text("Prix", M + w + 5, 26, { lineBreak: false });
56 + doc.image(UQO_LOGO_BLEU, M, 22, { height: 14 });
57 + doc.font("SG-Bold").fontSize(10).fillColor(INK)
58 + .text("Éval", M + 14 * UQO_RATIO + 4, 26, { lineBreak: false });
52 59 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)
53 − .text("RAPPORT D'ÉVALUATION DÉTAILLÉ — FORMAT PROFESSIONNEL", M + 70, 29, { characterSpacing: 0.8, lineBreak: false });
60 + .text("RAPPORT D'ÉVALUATION DÉTAILLÉ — CADRE PÉDAGOGIQUE UQO", M + 90, 29, { characterSpacing: 0.8, lineBreak: false });
54 61 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)
55 62 .text(`DOSSIER ${c.dossier}`, W - M - 160, 29, { width: 160, align: "right" });
56 63 doc.rect(M, 44, CW, 0.8).fill(INK);
@@ -60,8 +67,11 @@ function footer(c: Ctx, total: number) {
60 67 const { doc } = c;
61 68 doc.rect(M, H - 52, CW, 0.8).fill(INK);
62 69 doc.font("JB-Reg").fontSize(6).fillColor(INK3).text(
63 − "ESTIMATION STATISTIQUE AUTOMATISÉE (AVM) — NE CONSTITUE PAS UN RAPPORT D'ÉVALUATION AU SENS DES NORMES DE L'OEAQ · VRAI-PRIX.COM · CONTACT@SPBOUCHER.AI",
70 + "ESTIMATION STATISTIQUE AUTOMATISÉE (AVM) À VOCATION PÉDAGOGIQUE — NE CONSTITUE PAS UN RAPPORT D'ÉVALUATION AU SENS DES NORMES DE L'OEAQ · UQO-EVAL.APP",
64 71 M, H - 44, { width: CW - 50, characterSpacing: 0.3, lineBreak: false });
72 + doc.font("JB-Reg").fontSize(6).fillColor(INK3).text(
73 + "COURS IMM1003 · IMM1033 — SIMON-PIERRE BOUCHER, UQO",
74 + M, H - 36, { width: CW - 50, characterSpacing: 0.3, lineBreak: false });
65 75 doc.font("JB-Bold").fontSize(7).fillColor(INK)
66 76 .text(`${c.page} / ${total}`, W - M - 40, H - 45, { width: 40, align: "right" });
67 77 }
@@ -116,10 +126,10 @@ export async function buildProReport(
116 126 const s = u.specs;
117 127 const r = data.result;
118 128 const generated = new Date().toISOString().slice(0, 10);
119 − const dossier = `VP-${u.id.slice(0, 8)}-${generated.replaceAll("-", "")}`;
129 + const dossier = `UQO-${u.id.slice(0, 8)}-${generated.replaceAll("-", "")}`;
120 130 const TOTAL = 6;
121 131
122 − const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: `Rapport d'évaluation détaillé — ${u.adresse ?? ""}` } });
132 + const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: `UQO Éval — Rapport d'évaluation détaillé — ${u.adresse ?? ""}`, Author: "UQO Éval — cours IMM1003 · IMM1033 (Simon-Pierre Boucher, UQO)" } });
123 133 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));
124 134 doc.registerFont("SG-Med", F("SpaceGrotesk-Medium.ttf"));
125 135 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));
@@ -135,13 +145,14 @@ export async function buildProReport(
135 145 doc.rect(0, 0, W, H).fill("#ffffff");
136 146 doc.rect(0, 0, W, 6).fill(INK);
137 147 doc.rect(0, 6, W, 2).fill(LIME);
138 − // logo sobre
139 − doc.font("SG-Bold").fontSize(26).fillColor(INK).text("Vrai", M, 70, { lineBreak: false });
140 − const lw = doc.widthOfString("Vrai");
141 − doc.rect(M + lw + 4, 68, doc.widthOfString("Prix") + 14, 32).fill(INK);
142 − doc.fillColor(LIME).text("Prix", M + lw + 11, 70, { lineBreak: false });
148 + // logo officiel UQO (bleu PMS 7469, proportions natives) + wordmark Éval
149 + doc.image(UQO_LOGO_BLEU, M, 62, { height: 38 });
150 + doc.font("SG-Bold").fontSize(26).fillColor(INK)
151 + .text("Éval", M + 38 * UQO_RATIO + 10, 72, { lineBreak: false });
143 152 doc.font("JB-Reg").fontSize(7.5).fillColor(INK3)
144 − .text("MOTEUR D'ÉVALUATION IMMOBILIÈRE STATISTIQUE · QUÉBEC", M, 108, { characterSpacing: 1.2 });
153 + .text("LABORATOIRE PÉDAGOGIQUE D'ÉVALUATION IMMOBILIÈRE · UNIVERSITÉ DU QUÉBEC EN OUTAOUAIS", M, 108, { characterSpacing: 1.2 });
154 + doc.font("JB-Bold").fontSize(7).fillColor(GREEN)
155 + .text("COURS IMM1003 — ÉLÉMENTS D'ÉVALUATION IMMOBILIÈRE · IMM1033 — MÉTHODES DU COÛT EN ÉVALUATION IMMOBILIÈRE", M, 122, { characterSpacing: 0.6 });
145 156
146 157 doc.rect(M, 168, CW, 1.4).fill(INK);
147 158 doc.font("SG-Bold").fontSize(30).fillColor(INK)
@@ -174,10 +185,10 @@ export async function buildProReport(
174 185 // préparé pour / par
175 186 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("PRÉPARÉ POUR", M, 585, { characterSpacing: 1 });
176 187 doc.rect(M, 610, 220, 0.9).fill(INK2);
177 − doc.font("Inter").fontSize(7).fillColor(INK3).text("(destinataire / institution financière)", M, 615);
188 + doc.font("Inter").fontSize(7).fillColor(INK3).text("(étudiante ou étudiant / groupe-cours)", M, 615);
178 189 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("PRÉPARÉ PAR", M + 300, 585, { characterSpacing: 1 });
179 − doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Vrai-Prix — moteur d'évaluation statistique", M + 300, 597);
180 − doc.font("Inter").fontSize(8).fillColor(INK2).text("Simon-Pierre Boucher · contact@spboucher.ai", M + 300, 611);
190 + doc.font("SG-Bold").fontSize(10).fillColor(INK).text("UQO Éval — moteur d'évaluation statistique", M + 300, 597);
191 + doc.font("Inter").fontSize(8).fillColor(INK2).text("Simon-Pierre Boucher, enseignant (UQO)", M + 300, 611);
181 192 doc.font("Inter").fontSize(8).fillColor(INK2).text(`Dossier ${dossier} · Généré le ${generated}`, M + 300, 623);
182 193
183 194 // avis
@@ -386,15 +397,15 @@ export async function buildProReport(
386 397 y = newPage(c, TOTAL);
387 398 y = sectionTitle(c, "7.", "Hypothèses et conditions limitatives", y);
388 399 const conds = [
389 − "Le présent rapport est généré automatiquement par le moteur d'évaluation statistique Vrai-Prix (modèle d'évaluation de masse, AVM). Il ne constitue pas un rapport d'évaluation au sens des normes de pratique professionnelle de l'Ordre des évaluateurs agréés du Québec (OEAQ) et ne peut être présenté comme tel.",
400 + "Le présent rapport est généré automatiquement par le moteur d'évaluation statistique UQO Éval (modèle d'évaluation de masse, AVM), dans un cadre pédagogique universitaire. Il ne constitue pas un rapport d'évaluation au sens des normes de pratique professionnelle de l'Ordre des évaluateurs agréés du Québec (OEAQ) et ne peut être présenté comme tel.",
390 401 "Aucune inspection physique de la propriété n'a été effectuée. L'état d'entretien, la qualité des finis, les rénovations, les dépendances et tout vice apparent ou caché ne sont pas considérés.",
391 402 "Les caractéristiques utilisées proviennent du rôle d'évaluation foncière (MAMH, données ouvertes) et de transactions publiées. Leur exactitude et leur mise à jour ne sont pas garanties.",
392 403 "Le droit évalué est présumé être la pleine propriété, libre de toute hypothèque, servitude, charge ou restriction inhabituelle. Aucune vérification de titres n'a été réalisée.",
393 404 "La conformité aux lois, règlements de zonage et permis est présumée. Aucune étude environnementale n'a été effectuée ; la propriété est présumée exempte de contamination.",
394 405 "La valeur conclue vaut à la date d'effet uniquement. Les marchés immobiliers évoluent ; l'estimation ne doit pas être utilisée à une autre date.",
395 406 "La fourchette P10-P90 et l'indice de fiabilité font partie intégrante de la conclusion et doivent être communiqués avec elle.",
396 − "Ce rapport est destiné au destinataire indiqué en page titre, aux fins d'information et d'appui à une démarche de financement. Toute autre utilisation ou distribution engage la seule responsabilité de l'utilisateur.",
397 − "Pour une décision de financement finale, une confirmation par un évaluateur agréé (OEAQ) mandaté par l'institution financière est recommandée.",
407 + "Ce rapport est destiné à un usage pédagogique, universitaire et expérimental (cours IMM1003 et IMM1033, UQO). Toute autre utilisation ou distribution engage la seule responsabilité de l'utilisateur.",
408 + "Pour toute décision réelle (financement, transaction), une évaluation par un évaluateur agréé (OEAQ) est requise.",
398 409 ];
399 410 conds.forEach((txt, i) => {
400 411 doc.font("JB-Bold").fontSize(7).fillColor(GREEN).text(`7.${i + 1}`, M, y + 1, { lineBreak: false });
@@ -406,17 +417,18 @@ export async function buildProReport(
406 417 y = sectionTitle(c, "8.", "Attestation", y + 2);
407 418 y = para(c,
408 419 "J'atteste que les données, méthodes et pondérations décrites dans ce rapport sont celles réellement employées par le moteur " +
409 − "Vrai-Prix pour produire la conclusion de valeur ; que la méthodologie complète, ses métriques de validation et ses limites " +
410 − "sont publiées à vrai-prix.com/methodologie ; et que ni Vrai-Prix ni son exploitant n'ont d'intérêt présent ou futur dans la " +
411 − "propriété évaluée, ni de lien de dépendance avec les parties à une éventuelle transaction.", y);
420 + "UQO Éval pour produire la conclusion de valeur ; que la méthodologie complète, ses métriques de validation et ses limites " +
421 + "sont publiées à uqo-eval.app/methodologie ; et que ni UQO Éval ni son exploitant n'ont d'intérêt présent ou futur dans la " +
422 + "propriété évaluée, ni de lien de dépendance avec les parties à une éventuelle transaction. " + COURS, y);
412 423 y += 14;
413 424 doc.rect(M, y + 26, 200, 0.9).fill(INK);
414 − doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Vrai-Prix", M, y + 32);
425 + doc.image(UQO_LOGO_BLEU, M, y + 32, { height: 16 });
426 + doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Éval", M + 16 * UQO_RATIO + 5, y + 36, { lineBreak: false });
415 427 doc.font("Inter").fontSize(8).fillColor(INK2)
416 − .text("Simon-Pierre Boucher · contact@spboucher.ai", M, y + 46)
417 − .text(`Dossier ${dossier} · ${generated}`, M, y + 58);
428 + .text("Simon-Pierre Boucher, enseignant — UQO", M, y + 52)
429 + .text(`Dossier ${dossier} · ${generated}`, M, y + 64);
418 430 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)
419 − .text("ANNEXE DISPONIBLE : RAPPORT STANDARD (PLAN DES COMPARABLES, LISTE COMPLÈTE DES VENTES) — VRAI-PRIX.COM", M, y + 82, { characterSpacing: 0.5 });
431 + .text("ANNEXE DISPONIBLE : RAPPORT STANDARD (PLAN DES COMPARABLES, LISTE COMPLÈTE DES VENTES) — UQO-EVAL.APP", M, y + 86, { characterSpacing: 0.5 });
420 432
421 433 doc.end();
422 434 return done;
modified src/lib/report-stats.ts +39 −44
@@ -1,12 +1,12 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Rapport statistique provincial — PDF vectoriel au GABARIT COMMUN GROUPE KA
3 + * Rapport statistique provincial — PDF vectoriel (gabarit UQO Éval)
4 4 * v2 (src/ka/stats/SPEC.md §3), moteur pdfkit (équivalent TS de kapdf.py) :
5 − * couverture estampillée « GROUPE KA · RAPPORT STATISTIQUE », en-tête/pied
5 + * couverture estampillée « UQO ÉVAL · RAPPORT STATISTIQUE », en-tête/pied
6 6 * normalisés, sommaire paginé, graphiques 100 % VECTORIELS (courbes, aires,
7 7 * barres, multi-courbes à motifs distincts, empilées, anneaux, jauges,
8 8 * calendrier), statistiques de séries (min/max/moy/méd/σ), tableaux zébrés
9 − * paginés, records, page de fin avec coordonnées (src/ka/ecosystem.json).
9 + * paginés, records, page de fin avec mentions pédagogiques.
10 10 *
11 11 * 5 MODES (SPEC §3) — un mode inconnu retombe sur « complet » :
12 12 * complet · synthese · tendances · repartitions · donnees
@@ -17,19 +17,18 @@ import path from "path";
17 17 import type { ProvStats } from "@/components/StatsView";
18 18 import { getDashboard, buildRecords, PERIOD_LABEL, type KaSerie, type KaRecord } from "@/lib/stats-dashboard";
19 19 import stats from "@/data/stats.json";
20 −import eco from "@/ka/ecosystem.json";
21 20
22 21 const INK = "#141814";
23 22 const INK2 = "#4d5551";
24 23 const INK3 = "#8b928c";
25 24 const PAPER = "#f5f3ee";
26 25 const SURFACE2 = "#faf9f5";
27 −const GREEN = "#9e2a25"; // accentDeep Vrai-Prix (ecosystem.json)
28 −const GREEN_DEEP = "#771f1b";
29 −const LIME = "#ff5148"; // accent Vrai-Prix (ecosystem.json)
26 +const GREEN = "#005F85"; // bleu UQO (PMS 7469)
27 +const GREEN_DEEP = "#00465F"; // bleu UQO profond
28 +const LIME = "#2BCCAE"; // turquoise UQO (accent web officiel)
30 29 const WHITE = "#ffffff";
31 30
32 −/* A4 portrait (gabarit Groupe-KA) */
31 +/* A4 portrait */
33 32 const W = 595.28;
34 33 const H = 841.89;
35 34 const M = 44;
@@ -72,15 +71,15 @@ const MODE_FR: Record<ReportMode, string> = {
72 71 type Doc = InstanceType<typeof PDFDocument>;
73 72 type Dash = ReturnType<typeof getDashboard>;
74 73
75 −/** Wordmark Vrai-Prix : « Vrai- » encre + boîte encre / accent rouge #ff5148. */
74 +/** Wordmark UQO Éval : « UQO » encre + boîte encre / accent. */
76 75 function wordmark(doc: Doc, x: number, y: number, size = 24) {
77 76 doc.font("SG-Bold").fontSize(size);
78 − doc.fillColor(INK).text("Vrai-", x, y, { lineBreak: false });
79 − const w = doc.widthOfString("Vrai-");
77 + doc.fillColor(INK).text("UQO ", x, y, { lineBreak: false });
78 + const w = doc.widthOfString("UQO ");
80 79 const bx = x + w + 0.18 * size;
81 − const bw = doc.widthOfString("Prix") + 0.5 * size;
80 + const bw = doc.widthOfString("Éval") + 0.5 * size;
82 81 doc.roundedRect(bx, y - 0.14 * size, bw, 1.28 * size, 0.18 * size).fill(INK);
83 − doc.fillColor(LIME).text("Prix", bx + 0.25 * size, y, { lineBreak: false });
82 + doc.fillColor(LIME).text("Éval", bx + 0.25 * size, y, { lineBreak: false });
84 83 }
85 84
86 85 function kicker(doc: Doc, txt: string, x: number, y: number, size = 8) {
@@ -89,18 +88,18 @@ function kicker(doc: Doc, txt: string, x: number, y: number, size = 8) {
89 88 .text(txt.toUpperCase(), x + 2.5 * size + 6, y, { characterSpacing: 0.175 * size, lineBreak: false, width: CW - 2.5 * size - 6, height: size * 1.6, ellipsis: true });
90 89 }
91 90
92 −/** En-tête + pied normalisés Groupe-KA (numéros de page ajoutés en post-passe). */
91 +/** En-tête + pied normalisés (numéros de page ajoutés en post-passe). */
93 92 function chrome(doc: Doc) {
94 93 const year = new Date().getFullYear();
95 94 doc.rect(0, 0, W, H).fill(PAPER);
96 95 doc.font("SG-Bold").fontSize(9.5).fillColor(INK)
97 − .text("Groupe KA · Vrai-Prix", M, 22, { lineBreak: false });
96 + .text("UQO Éval", M, 22, { lineBreak: false });
98 97 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)
99 98 .text("RAPPORT STATISTIQUE", W - M - 180, 24.5, { width: 180, align: "right", characterSpacing: 0.8 });
100 99 doc.rect(M, 37, CW, 1.1).fill(INK);
101 100 doc.rect(M, H - 42, CW, 0.7).fill(INK3);
102 101 doc.font("JB-Reg").fontSize(6.8).fillColor(INK3)
103 − .text(`© Groupe-KA — ${year} — groupe-ka.com · ${PERIOD_LABEL}`, M, H - 34, { lineBreak: false });
102 + .text(`© UQO Éval — ${year} — www.uqo-eval.app · ${PERIOD_LABEL}`, M, H - 34, { lineBreak: false });
104 103 }
105 104
106 105 function card(doc: Doc, x: number, y: number, w: number, h: number, fill = "#ffffff") {
@@ -533,16 +532,16 @@ function recordsPdf(c: Cursor, records: KaRecord[], remember = true) {
533 532 function cover(doc: Doc, s: ProvStats, generated: string, modeLabel: string) {
534 533 doc.rect(0, 0, W, H).fill(PAPER);
535 534 doc.lineWidth(2).rect(28, 28, W - 56, H - 56).stroke(INK);
536 − kicker(doc, "Groupe KA · Rapport statistique", 64, 100, 10.5);
535 + kicker(doc, "UQO Éval · Rapport statistique", 64, 100, 10.5);
537 536 wordmark(doc, 64, 172, 46);
538 537 doc.font("Inter").fontSize(13).fillColor(INK2).text(
539 − "La valeur réelle de chaque propriété — rapport statistique provincial du parc immobilier québécois.",
538 + "Laboratoire pédagogique d'évaluation immobilière — rapport statistique provincial du parc immobilier québécois.",
540 539 64, 256, { width: W - 168, lineGap: 3 });
541 540 const rows: [string, string][] = [
542 541 ["Type de rapport", modeLabel],
543 542 ["Période couverte", "Instantané du rôle d'évaluation 2026 (millésime)"],
544 543 ["Généré le", `${generated} (heure de l'Est)`],
545 − ["Plateforme", "www.vrai-prix.com"],
544 + ["Plateforme", "www.uqo-eval.app"],
546 545 ["Données", `${num(s.unites)} propriétés · ${num(s.municipalites)} municipalités`],
547 546 ];
548 547 doc.rect(64, 318, 34, 3).fill(LIME);
@@ -556,10 +555,10 @@ function cover(doc: Doc, s: ProvStats, generated: string, modeLabel: string) {
556 555 });
557 556 doc.rect(28, H - 28 - 64, W - 56, 64).fill(INK);
558 557 const by = H - 28 - 64 + 24;
559 − doc.font("SG-Bold").fontSize(15).fillColor(WHITE).text("par Groupe ", 64, by, { lineBreak: false });
560 − doc.fillColor(LIME).text("KA", 64 + doc.widthOfString("par Groupe "), by, { lineBreak: false });
558 + doc.font("SG-Bold").fontSize(15).fillColor(WHITE).text("UQO ", 64, by, { lineBreak: false });
559 + doc.fillColor(LIME).text("Éval", 64 + doc.widthOfString("UQO "), by, { lineBreak: false });
561 560 doc.font("JB-Bold").fontSize(9.5).fillColor(LIME)
562 − .text("groupe-ka.com", W - 64 - 180, by + 4, { width: 180, align: "right", characterSpacing: 0.8 });
561 + .text("www.uqo-eval.app", W - 64 - 180, by + 4, { width: 180, align: "right", characterSpacing: 0.8 });
563 562 }
564 563
565 564 /* --------------------- VUE D'ENSEMBLE (le chiffre + comparaisons) --------------------- */
@@ -594,30 +593,26 @@ function overview(c: Cursor, s: ProvStats) {
594 593 /* ------------------------------ PAGE DE FIN ------------------------------ */
595 594 function finalPage(doc: Doc) {
596 595 let y = 60;
597 − kicker(doc, "Groupe KA · Contact", M, y);
596 + kicker(doc, "UQO Éval · À propos", M, y);
598 597 y += 20;
599 − doc.font("SG-Bold").fontSize(21).fillColor(INK).text("Coordonnées du Groupe KA", M, y);
598 + doc.font("SG-Bold").fontSize(21).fillColor(INK).text("UQO Éval — laboratoire pédagogique", M, y);
600 599 y += 40;
601 − eco.contacts.forEach((c) => {
602 − doc.font("SG-Bold").fontSize(12.5).fillColor(INK).text(c.email, M, y, { lineBreak: false });
603 − doc.font("JB-Reg").fontSize(8).fillColor(INK3).text(c.role.toUpperCase(), M, y + 17, { characterSpacing: 0.5, lineBreak: false });
604 − y += 42;
605 − });
606 − y += 6;
607 600 doc.font("JB-Bold").fontSize(9.5).fillColor(GREEN)
608 − .text("groupe-ka.com — le portail de l'écosystème ·Ka", M, y, { lineBreak: false });
601 + .text("www.uqo-eval.app — laboratoire pédagogique d'évaluation immobilière", M, y, { lineBreak: false });
609 602 y += 26;
610 603 doc.rect(M, y, 44, 3).fill(LIME);
611 604 y += 14;
612 605 doc.font("Inter").fontSize(10).fillColor(INK2)
613 − .text(eco.org.disclaimer + " Données lues à la source, rien d'inventé, tout est traçable.", M, y, { width: 420, lineGap: 3 });
606 + .text(
607 + "Plateforme à vocation pédagogique, universitaire et expérimentale. Données lues à la source, rien d'inventé, tout est traçable.",
608 + M, y, { width: 420, lineGap: 3 });
614 609 y = doc.y + 16;
615 610 doc.font("Inter").fontSize(8).fillColor(INK3).text(
616 611 "Mentions : rapport généré automatiquement à partir des données réelles de la plateforme au moment " +
617 612 "indiqué en couverture. Les valeurs sont des estimations statistiques (modèle hédonique, rôle " +
618 − "d'évaluation 2026) et ne remplacent pas une évaluation professionnelle. Conditions d'utilisation, " +
619 − "politique de confidentialité et protection des renseignements personnels (Loi 25) : " +
620 − "groupe-ka.com/conditions · /confidentialite · /loi-25.",
613 + "d'évaluation 2026) produites à des fins pédagogiques et expérimentales ; elles ne constituent pas " +
614 + "une évaluation immobilière professionnelle certifiée (OEAQ). Conditions d'utilisation et politique " +
615 + "de confidentialité (Loi 25) : www.uqo-eval.app/conditions · /confidentialite.",
621 616 M, y, { width: 420, lineGap: 2.5 });
622 617 wordmark(doc, M, H - 92, 15);
623 618 }
@@ -633,7 +628,7 @@ export async function buildStatsReport(mode: ReportMode = "complet"): Promise<Bu
633 628
634 629 const doc = new PDFDocument({
635 630 size: "A4", margin: 0, bufferPages: true,
636 − info: { Title: `Groupe KA · Vrai-Prix — Rapport statistique (${MODE_FR[mode]})`, Author: "Groupe KA — groupe-ka.com" },
631 + info: { Title: `UQO Éval — Rapport statistique (${MODE_FR[mode]})`, Author: "UQO Éval — www.uqo-eval.app" },
637 632 });
638 633 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));
639 634 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));
@@ -728,7 +723,7 @@ export async function buildStatsReport(mode: ReportMode = "complet"): Promise<Bu
728 723 y += 40;
729 724 const entries: [string, string][] = [
730 725 ...c.sections.map((sec) => [sec.label, String(sec.page)] as [string, string]),
731 − ["Coordonnées du Groupe KA & mentions", String(total)],
726 + ["À propos d'UQO Éval & mentions", String(total)],
732 727 ];
733 728 entries.slice(0, 16).forEach(([label, pg]) => {
734 729 doc.rect(M, y + 22, CW, 0.7).fill("#e3e1d9");
@@ -738,7 +733,7 @@ export async function buildStatsReport(mode: ReportMode = "complet"): Promise<Bu
738 733 });
739 734 y += 8;
740 735 doc.font("JB-Reg").fontSize(7).fillColor(INK3).text(
741 − `PÉRIODE : ${PERIOD_LABEL.toUpperCase()} · STATISTIQUES GÉNÉRÉES LE ${s.generated} · SOURCE : RÔLE D'ÉVALUATION FONCIÈRE + CORPUS DE VENTES + MODÈLE HÉDONIQUE VRAI-PRIX`,
736 + `PÉRIODE : ${PERIOD_LABEL.toUpperCase()} · STATISTIQUES GÉNÉRÉES LE ${s.generated} · SOURCE : RÔLE D'ÉVALUATION FONCIÈRE + CORPUS DE VENTES + MODÈLE HÉDONIQUE UQO ÉVAL`,
742 737 M, y, { characterSpacing: 0.4, width: CW });
743 738 }
744 739 for (let i = 1; i < total; i++) {
@@ -751,16 +746,16 @@ export async function buildStatsReport(mode: ReportMode = "complet"): Promise<Bu
751 746 return done;
752 747 }
753 748
754 −/** Nom de fichier normalisé Groupe-KA (SPEC §3) — pas de suffixe pour `complet`. */
749 +/** Nom de fichier normalisé — pas de suffixe pour `complet`. */
755 750 export function reportFilename(mode: ReportMode | "personnalise" = "complet"): string {
756 751 const today = new Date().toLocaleDateString("fr-CA", { timeZone: "America/Toronto" });
757 752 const suffix = mode === "complet" ? "" : `_${mode}`;
758 − return `groupe-ka_vrai-prix_stats_instantane${suffix}_${today}.pdf`;
753 + return `uqo-eval_stats_instantane${suffix}_${today}.pdf`;
759 754 }
760 755
761 756 /* ===================== v3 : rapports personnalisés (SPEC §3bis) =====================
762 757 Catalogue de blocs dérivé du dashboard + génération d'un PDF composé bloc
763 − par bloc (rendu au choix), même gabarit estampillé Groupe-KA. */
758 + par bloc (rendu au choix), même gabarit. */
764 759 export type CatalogBlock = {
765 760 key: string; section: string; title: string;
766 761 renders: string[]; default_render: string; count?: number;
@@ -929,7 +924,7 @@ export async function buildCustomReport(spec: CustomSpec): Promise<Buffer> {
929 924 }).replace(",", " ·");
930 925 const doc = new PDFDocument({
931 926 size: "A4", margin: 0, bufferPages: true,
932 − info: { Title: `Groupe KA · Vrai-Prix — ${label}`, Author: "Groupe KA — groupe-ka.com" },
927 + info: { Title: `UQO Éval — ${label}`, Author: "UQO Éval — www.uqo-eval.app" },
933 928 });
934 929 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));
935 930 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));
@@ -964,7 +959,7 @@ export async function buildCustomReport(spec: CustomSpec): Promise<Buffer> {
964 959 y += 40;
965 960 const entries: [string, string][] = [
966 961 ...c.sections.map((sec) => [sec.label, String(sec.page)] as [string, string]),
967 − ["Coordonnées du Groupe KA & mentions", String(total)],
962 + ["À propos d'UQO Éval & mentions", String(total)],
968 963 ];
969 964 entries.slice(0, 16).forEach(([lab, pg]) => {
970 965 doc.rect(M, y + 22, CW, 0.7).fill("#e3e1d9");
modified src/lib/report.ts +37 −27
@@ -1,21 +1,22 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Rapport PDF Vrai-Prix — 100 % vectoriel (pdfkit), identité « éditorial sharp » :
4 − * logo encre/lime, fiche complète du rôle, historique en barres, plan radar des
5 − * comparables dessiné dans le PDF, tableau des ajustements. Format Lettre.
3 + * Rapport PDF UQO Éval — vectoriel (pdfkit) + logo officiel UQO :
4 + * fiche complète du rôle, historique en barres, plan radar des comparables
5 + * dessiné dans le PDF, tableau des ajustements. Format Lettre.
6 + * Généré dans le cadre des cours IMM1003 et IMM1033 (UQO).
6 7 */
7 8 import PDFDocument from "pdfkit";
8 9 import path from "path";
9 10 import type { UnitEstimate } from "./estimator";
10 11
11 −const INK = "#141814";
12 +const INK = "#16181b";
12 13 const INK3 = "#8b928c";
13 −const PAPER = "#f5f3ee";
14 −const SURFACE2 = "#faf9f5";
15 −const GREEN = "#9e2a25";
16 −const GREEN_DEEP = "#771f1b";
17 −const LIME = "#ff5148";
18 −const LIME_SOFT = "#ffe3e0";
14 +const PAPER = "#f4f5f3";
15 +const SURFACE2 = "#f9fafa";
16 +const GREEN = "#005F85"; // bleu UQO (PMS 7469)
17 +const GREEN_DEEP = "#00465F";
18 +const LIME = "#2BCCAE"; // turquoise UQO
19 +const LIME_SOFT = "#d9f4ee";
19 20 const AMBER_SOFT = "#fdf3e2";
20 21 const DANGER_SOFT = "#fbe9e7";
21 22 const LINE = "#dedcd4";
@@ -25,6 +26,12 @@ const H = 792;
25 26 const M = 44; // marge
26 27
27 28 const F = (f: string) => path.join(process.cwd(), "assets", "fonts", f);
29 +const UQO_LOGO_BLANC = path.join(process.cwd(), "assets", "uqo", "UQO_blanc.png");
30 +
31 +/** Mention pédagogique obligatoire sur tous les rapports UQO Éval. */
32 +const COURS =
33 + "Rapport généré dans le cadre des cours IMM1003 — Éléments d'évaluation immobilière et " +
34 + "IMM1033 — Méthodes du coût en évaluation immobilière, enseignés par Simon-Pierre Boucher (UQO).";
28 35
29 36 const fmt = (v: number | null | undefined) =>
30 37 v == null
@@ -45,19 +52,14 @@ const CONF_TXT: Record<string, string> = {
45 52
46 53 type Doc = InstanceType<typeof PDFDocument>;
47 54
55 +/** Logo officiel UQO (blanc, pour bandeau encre) + wordmark « Éval ».
56 + * Proportions du logo respectées (jamais déformé — hauteur seule imposée). */
48 57 function logo(doc: Doc, x: number, y: number, scale = 1) {
49 − doc.save();
50 − doc.font("SG-Bold").fontSize(24 * scale);
51 − doc.fillColor(PAPER).text("Vrai", x, y, { lineBreak: false });
52 − const w = doc.widthOfString("Vrai");
53 − const bx = x + w + 4 * scale;
54 − doc.save();
55 − doc.rotate(-3, { origin: [bx, y + 12 * scale] });
56 − const bw = doc.widthOfString("Prix") + 12 * scale;
57 − doc.roundedRect(bx, y - 3 * scale, bw, 30 * scale, 5 * scale).fill(LIME);
58 − doc.fillColor(INK).text("Prix", bx + 6 * scale, y, { lineBreak: false });
59 − doc.restore();
60 − doc.restore();
58 + const h = 26 * scale; // hauteur du logo — la largeur suit le ratio natif (2123×853)
59 + const w = h * (2123 / 853);
60 + doc.image(UQO_LOGO_BLANC, x, y, { height: h });
61 + doc.font("SG-Bold").fontSize(22 * scale).fillColor(LIME)
62 + .text("Éval", x + w + 8 * scale, y + h - 22 * scale, { lineBreak: false });
61 63 }
62 64
63 65 function kicker(doc: Doc, txt: string, x: number, y: number, color = GREEN) {
@@ -71,9 +73,13 @@ function pageFrame(doc: Doc, page: number, total: number, generated: string) {
71 73 // filet de bas de page
72 74 doc.rect(M, H - 46, W - 2 * M, 1.2).fill(INK);
73 75 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3).text(
74 − "VRAI-PRIX.COM — ESTIMATION STATISTIQUE À TITRE INDICATIF · NE REMPLACE PAS UNE ÉVALUATION PROFESSIONNELLE · SIMON-PIERRE BOUCHER · CONTACT@SPBOUCHER.AI",
76 + "UQO-EVAL.APP — PLATEFORME PÉDAGOGIQUE · ESTIMATION STATISTIQUE À TITRE INDICATIF · NE REMPLACE PAS UNE ÉVALUATION PROFESSIONNELLE CERTIFIÉE (OEAQ)",
75 77 M, H - 38, { width: W - 2 * M - 60, characterSpacing: 0.4, lineBreak: false }
76 78 );
79 + doc.font("JB-Reg").fontSize(6.5).fillColor(INK3).text(
80 + "COURS IMM1003 · IMM1033 — SIMON-PIERRE BOUCHER, UQO",
81 + M, H - 29, { width: W - 2 * M - 60, characterSpacing: 0.4, lineBreak: false }
82 + );
77 83 doc.font("JB-Bold").fontSize(7.5).fillColor(INK)
78 84 .text(`${page} / ${total}`, W - M - 40, H - 39, { width: 40, align: "right" });
79 85 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)
@@ -167,7 +173,7 @@ export async function buildReport(data: UnitEstimate): Promise<Buffer> {
167 173 const r = data.result;
168 174 const generated = new Date().toISOString().slice(0, 16).replace("T", " ");
169 175
170 − const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: `Vrai-Prix — ${u.adresse ?? ""}` } });
176 + const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: `UQO Éval — ${u.adresse ?? ""}`, Author: "UQO Éval — cours IMM1003 · IMM1033 (Simon-Pierre Boucher, UQO)" } });
171 177 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));
172 178 doc.registerFont("SG-Med", F("SpaceGrotesk-Medium.ttf"));
173 179 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));
@@ -185,7 +191,9 @@ export async function buildReport(data: UnitEstimate): Promise<Buffer> {
185 191 doc.rect(0, 0, W, 96).fill(INK);
186 192 logo(doc, M, 30);
187 193 doc.font("JB-Reg").fontSize(7).fillColor(LIME)
188 − .text("RAPPORT D'ÉVALUATION · QUÉBEC", M, 66, { characterSpacing: 1.6, lineBreak: false });
194 + .text("LABORATOIRE PÉDAGOGIQUE D'ÉVALUATION IMMOBILIÈRE · RAPPORT D'ÉVALUATION", M, 66, { characterSpacing: 1.2, lineBreak: false });
195 + doc.font("JB-Reg").fontSize(6).fillColor("#8fb8c9")
196 + .text("COURS IMM1003 · IMM1033 — SIMON-PIERRE BOUCHER, UQO", M, 78, { characterSpacing: 1, lineBreak: false });
189 197 doc.font("JB-Reg").fontSize(7).fillColor("#9aa39c")
190 198 .text(`ID ${u.id}`, W - M - 200, 34, { width: 200, align: "right" })
191 199 .text(`GÉNÉRÉ LE ${generated}`, W - M - 200, 46, { width: 200, align: "right" })
@@ -344,7 +352,7 @@ export async function buildReport(data: UnitEstimate): Promise<Buffer> {
344 352
345 353 // méthodo
346 354 const my = Math.max(legY + 20, ty + 14);
347 − card(doc, M, my, W - 2 * M, 92, { fill: INK, shadow: false });
355 + card(doc, M, my, W - 2 * M, 104, { fill: INK, shadow: false });
348 356 doc.font("JB-Bold").fontSize(7).fillColor(LIME).text("MÉTHODOLOGIE", M + 16, my + 12, { characterSpacing: 1.4 });
349 357 doc.font("Inter").fontSize(7.8).fillColor("#e8e6df").text(
350 358 "L'estimation combine (1) un modèle hédonique de gradient boosting entraîné sur ~690 000 transactions québécoises " +
@@ -354,6 +362,8 @@ export async function buildReport(data: UnitEstimate): Promise<Buffer> {
354 362 "centiles du modèle recentrés sur l'estimation finale. L'indice de confiance (A-D) reflète le nombre de comparables, " +
355 363 "leur dispersion et la largeur de la fourchette. Validation selon la norme IAAO sur études de ratios.",
356 364 M + 16, my + 26, { width: W - 2 * M - 32, lineGap: 1.5 });
365 + doc.font("JB-Reg").fontSize(6.8).fillColor(LIME)
366 + .text(COURS.toUpperCase(), M + 16, my + 88, { width: W - 2 * M - 32, characterSpacing: 0.2, height: 14, ellipsis: true });
357 367
358 368 doc.end();
359 369 return done;
modified src/lib/seo.ts +3 −3
@@ -1,10 +1,10 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 // Constantes et aides SEO : URL canonique du site, slugs, libellés de types.
3 3
4 4 export const SITE_URL =
5 − process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.vrai-prix.com";
5 + process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.uqo-eval.app";
6 6
7 −export const SITE_NAME = "Vrai-Prix";
7 +export const SITE_NAME = "UQO Éval";
8 8
9 9 /** Slug URL d'un nom de municipalité (unique sur les 1 097 noms — vérifié). */
10 10 export function slugify(name: string): string {
modified src/lib/stats-dashboard.ts +6 −6
@@ -1,12 +1,12 @@
1 −// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
1 +// UQO Éval — laboratoire pédagogique d'évaluation immobilière
2 2 /**
3 − * Contrat commun /api/stats/dashboard v2 du Groupe KA (voir src/ka/stats/SPEC.md),
3 + * Contrat /api/stats/dashboard v2 (voir src/ka/stats/SPEC.md),
4 4 * construit depuis src/data/stats.json (agrégats historiques du rôle) et
5 5 * src/data/stats-v2.json (agrégats enrichis : distributions, transactions
6 6 * mensuelles/quotidiennes, indice de marché — générés par
7 7 * scripts/build-stats-v2.mjs depuis data/vraiprix.db, données 100 % réelles).
8 8 *
9 − * Particularité Vrai-Prix : le rôle d'évaluation est un INSTANTANÉ (millésime
9 + * Particularité UQO Éval : le rôle d'évaluation est un INSTANTANÉ (millésime
10 10 * 2026) — `period` est accepté (contrat commun) mais non applicable ; les
11 11 * vraies séries temporelles sont les millésimes 2021-2026, le corpus de
12 12 * ventes réelles (mensuel, 2021-01 → 2026-07) et l'indice de marché mensuel.
@@ -139,7 +139,7 @@ export function buildRecords(s: ProvStats): KaRecord[] {
139 139 }
140 140 if (V.rec?.max_est) {
141 141 out.push({
142 − label: "Estimation Vrai-Prix la plus élevée",
142 + label: "Estimation UQO Éval la plus élevée",
143 143 value: `${V.rec.max_est.municipalite} — ${compact(V.rec.max_est.v)}`,
144 144 });
145 145 }
@@ -197,7 +197,7 @@ function buildDashboard() {
197 197 id: "valeur_totale", label: "Valeur totale du parc (2026)", value: compact(s.valeur_totale_2026),
198 198 delta_pct: prev ? delta(last.total, prev.total) : null, direction: "up",
199 199 spark: ta.map((a) => ({ t: String(a.year), v: a.total })),
200 − help: "Somme des estimations Vrai-Prix — variation vs millésime 2025.",
200 + help: "Somme des estimations UQO Éval — variation vs millésime 2025.",
201 201 },
202 202 {
203 203 id: "valeur_moyenne", label: "Valeur moyenne (2026)", value: money(u.avg_est2026),
@@ -386,7 +386,7 @@ function buildDashboard() {
386 386 label: PERIOD_LABEL,
387 387 applicable: false,
388 388 note:
389 − "Les données de Vrai-Prix sont un instantané du rôle d'évaluation foncière (millésime 2026) : " +
389 + "Les données de UQO Éval sont un instantané du rôle d'évaluation foncière (millésime 2026) : " +
390 390 "le paramètre `period` est accepté mais sans effet. Les séries temporelles réelles servies sont " +
391 391 "les millésimes 2021-2026, le corpus de ventes mensuel (2021-01 → 2026-07) et l'indice de marché.",
392 392 },
added vendor/groupe-ka-ka-maps-0.1.0.tgz +0 −0

Binary file not shown.