SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
5.5 KB · 140 lines tsx
Raw Blame History
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