Python 67%
TypeScript 18.2%
CSS 14.4%
1// -----------------------------------------------------------------------------2// House-Ka — Homes-for-sale aggregator (Canada outside Québec, Ontario first)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// pages/Stats.tsx : market & platform statistics — totals, per-source volumes,5// data quality (completeness, quarantine), recent syncs.6// -----------------------------------------------------------------------------7import { useEffect, useState } from "react";8import {9 Stats, fetchSources, fetchStats, registerSourceNames, sourceName,10} from "../api";1112const n = (v: number | null | undefined) =>13 v == null ? "—" : Math.round(v).toLocaleString("en-CA");1415export default function StatsPage() {16 const [stats, setStats] = useState<Stats | null>(null);17 const [error, setError] = useState<string | null>(null);1819 useEffect(() => {20 document.title = "Canadian housing market statistics | House-Ka";21 fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {});22 fetchStats().then(setStats).catch((e) => setError(String(e)));23 }, []);2425 if (error) return <div className="notice container"><h2>Could not load the stats</h2><p>{error}</p></div>;26 if (!stats) return <div className="notice container">Loading…</div>;2728 const q = stats.qualite;29 const syncs = (stats.recent_syncs ?? []).slice(0, 20);3031 return (32 <div className="container stats-page">33 <span className="kicker">Live data</span>34 <h1>Platform statistics</h1>3536 <div className="mtg-resultat" style={{ marginTop: 18 }}>37 <div className="mtg-kpi">38 <span className="mtg-kpi-k">Homes for sale</span>39 <span className="mtg-kpi-v">{n(stats.total)}</span>40 <span className="mtg-kpi-sub">published, deduplicated</span>41 </div>42 <div className="mtg-kpi">43 <span className="mtg-kpi-k">Cities & towns</span>44 <span className="mtg-kpi-v">{n(stats.cities)}</span>45 </div>46 <div className="mtg-kpi">47 <span className="mtg-kpi-k">Average price</span>48 <span className="mtg-kpi-v">${n(stats.avg_price)}</span>49 <span className="mtg-kpi-sub">from ${n(stats.min_price)} to ${n(stats.max_price)}</span>50 </div>51 <div className="mtg-kpi">52 <span className="mtg-kpi-k">Sources</span>53 <span className="mtg-kpi-v">{n(stats.sources)}</span>54 <span className="mtg-kpi-sub">CREA DDF brokerage sites</span>55 </div>56 </div>5758 {q && (59 <section className="f-bloc">60 <h2>Data quality</h2>61 <p className="sub">62 Every listing gets a completeness score (photos, description,63 specs, coordinates). Listings without a plausible price or a known64 city are quarantined until the next enrichment pass completes them.65 </p>66 <div className="mtg-resultat">67 <div className="mtg-kpi">68 <span className="mtg-kpi-k">Active listings</span>69 <span className="mtg-kpi-v">{n(q.actives)}</span>70 </div>71 <div className="mtg-kpi">72 <span className="mtg-kpi-k">Published</span>73 <span className="mtg-kpi-v">{n(q.publiees)}</span>74 </div>75 <div className="mtg-kpi">76 <span className="mtg-kpi-k">In quarantine</span>77 <span className="mtg-kpi-v">{n(q.quarantaine)}</span>78 </div>79 <div className="mtg-kpi">80 <span className="mtg-kpi-k">Avg. completeness</span>81 <span className="mtg-kpi-v">{q.completude_moyenne ?? "—"}</span>82 <span className="mtg-kpi-sub">score /100</span>83 </div>84 </div>8586 {q.par_source.length > 0 && (87 <div className="rooms-wrap">88 <table className="rooms">89 <thead>90 <tr><th>Source</th><th>Listings</th><th>Published</th><th>Completeness</th><th>Flags</th></tr>91 </thead>92 <tbody>93 {q.par_source.map((s) => (94 <tr key={s.source}>95 <td>{sourceName(s.source)}</td>96 <td>{n(s.n)}</td>97 <td>{n(s.publiees)}</td>98 <td>{s.completude ?? "—"}</td>99 <td>{n(s.anomalies)}</td>100 </tr>101 ))}102 </tbody>103 </table>104 </div>105 )}106 </section>107 )}108109 {syncs.length > 0 && (110 <section className="f-bloc">111 <h2>Recent syncs</h2>112 <div className="rooms-wrap">113 <table className="rooms">114 <thead>115 <tr><th>Source</th><th>When</th><th>Found</th><th>Added</th><th>Updated</th><th>Removed</th></tr>116 </thead>117 <tbody>118 {syncs.map((s, i) => (119 <tr key={i} className={s.ok ? "" : "mtg-stale"}>120 <td>{sourceName(s.source)}</td>121 <td>{new Date((s.ts > 1e12 ? s.ts : s.ts * 1000)).toLocaleString("en-CA")}</td>122 <td>{n(s.found)}</td>123 <td>{n(s.added)}</td>124 <td>{n(s.updated)}</td>125 <td>{n(s.removed)}</td>126 </tr>127 ))}128 </tbody>129 </table>130 </div>131 <p className="fine">132 The connectors re-sync on their own, around the clock. A failed133 sync never wipes data — the last valid state is kept.134 </p>135 </section>136 )}137 </div>138 );139}140